This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner Verpflichtung oder Garantie irgendeiner Art verbunden. Autoren und Verlag übernehmen infolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen – oder Teilen davon – entsteht. Ebenso übernehmen Autoren und Verlag keine Gewähr dafür, dass beschriebene Verfahren usw. frei von Schutzrechten Dritter sind. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Buch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Bibliografische Information Der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Inhalt Vorwort ................................................................................................................................ 1 Danksagungen...................................................................................................................................... 2 Über dieses Buch................................................................................................................ 5 Für wen ist dieses Buch gedacht? ........................................................................................................ 5 Weitere Informationen über das Zend Framework .............................................................................. 6 Roadmap .............................................................................................................................................. 6 Codekonventionen und Downloads ..................................................................................................... 8 Author Online ...................................................................................................................................... 8 Über die Buchreihe .............................................................................................................................. 9 Teil I – Die Grundlagen ..................................................................................................... 11 1 1.1 1.2
1.3
1.4
Das Zend Framework – Eine Einführung............................................................. 13 Die Struktur von PHP-Websites ............................................................................................ 14 Gründe für das Zend Framework........................................................................................... 16 1.2.1 Alles ist gleich out of the box vorhanden ................................................................. 16 1.2.2 Modernes Design ..................................................................................................... 17 1.2.3 Leicht zu erlernen .................................................................................................... 17 1.2.4 Vollständige Dokumentation ................................................................................... 18 1.2.5 Einfachere Entwicklung........................................................................................... 19 1.2.6 Schnelle Entwicklung .............................................................................................. 19 1.2.7 Strukturierter, leicht zu pflegender Code ................................................................. 19 Was ist das Zend Framework?............................................................................................... 20 1.3.1 Woher stammt das Framework? .............................................................................. 20 1.3.2 Was ist darin enthalten?........................................................................................... 21 Die Designphilosophie des Zend Frameworks ...................................................................... 27 1.4.1 Komponenten von hoher Qualität ............................................................................ 27 1.4.2 Pragmatismus und Flexibilität ................................................................................. 28 1.4.3 Saubere Klärung der Rechte auf geistiges Eigentum ............................................... 28 1.4.4 Support von Zend Technologies .............................................................................. 28
V
Inhalt 1.5 1.6
Alternative PHP-Frameworks ................................................................................................29 Zusammenfassung..................................................................................................................30
2 2.1
Hello Zend Framework! ......................................................................................... 31 Das Designpattern Model-View-Controller ...........................................................................32 2.1.1 Das Model ................................................................................................................33 2.1.2 Die View ..................................................................................................................33 2.1.3 Der Controller...........................................................................................................33 Die Anatomie einer Zend Framework-Applikation................................................................34 2.2.1 Das Verzeichnis application .....................................................................................34 2.2.2 Das Verzeichnis library ............................................................................................35 2.2.3 Das Verzeichnis tests................................................................................................35 2.2.4 Das Verzeichnis public.............................................................................................35 Hello World: Datei für Datei..................................................................................................36 2.3.1 Bootstrapping ...........................................................................................................36 2.3.2 Apache .htaccess.......................................................................................................38 2.3.3 Index-Controller .......................................................................................................39 2.3.4 View-Template .........................................................................................................40 Wie MVC im Zend Framework angewendet wird .................................................................42 2.4.1 Der Controller im Zend Framework .........................................................................43 2.4.2 Arbeit mit dem Zend_View ......................................................................................47 2.4.3 Das Model in MVC ..................................................................................................51 Zusammenfassung..................................................................................................................55
2.2
2.3
2.4
2.5
Teil II – Eine Basisapplikation.......................................................................................... 57 3 3.1
3.2
3.3
3.4 4 4.1 4.2 4.3
VI
Websites mit dem Zend Framework erstellen..................................................... 59 Erste Planungsarbeiten für eine Website ................................................................................60 3.1.1 Die Ziel der Site........................................................................................................60 3.1.2 Das Design der Benutzerschnittstelle .......................................................................62 3.1.3 Den Code planen ......................................................................................................64 Die ersten Programmzeilen ....................................................................................................65 3.2.1 Die Verzeichnisstruktur............................................................................................65 3.2.2 Die Bootstrap-Klasse................................................................................................66 3.2.3 Der Start der Applikation..........................................................................................71 Die Homepage........................................................................................................................71 3.3.1 Die grundlegenden Models.......................................................................................72 3.3.2 Tests der Models.......................................................................................................74 3.3.3 Der Homepage-Controller ........................................................................................77 Zusammenfassung..................................................................................................................82 Die View erstellen .................................................................................................. 83 Die Patterns Two-Step-View und Composite-View...............................................................84 Zend_Layout und die Arbeit mit Views .................................................................................85 Die Integration von Zend_Layout in Places ...........................................................................86 4.3.1 Setup.........................................................................................................................86 4.3.2 Layout-Skripte..........................................................................................................88
Inhalt
4.4
4.5 5 5.1
5.2 5.3 5.4
5.5
5.6 6 6.1
6.2
6.3
6.4 7 7.1
7.2
4.3.3 Allgemeine Actions anhand von Platzhaltern .......................................................... 92 4.3.4 Das View-Skript für die Homepage......................................................................... 96 Fortgeschrittene View-Hilfsklassen....................................................................................... 98 4.4.1 Die Integration von Controllern............................................................................... 99 4.4.2 Die Verwaltung der View-Skripte ......................................................................... 101 4.4.3 Hilfsklassen für HTML-Kopfzeilen....................................................................... 102 Zusammenfassung ............................................................................................................... 107 Ajax ....................................................................................................................... 109 Kurze Einführung in Ajax ................................................................................................... 109 5.1.1 Definition von Ajax ............................................................................................... 110 5.1.2 Ajax in Webapplikationen ..................................................................................... 111 Ein einfache Beispiel für Ajax............................................................................................. 113 Die Arbeit mit Client-Libraries für Ajax ............................................................................. 116 Ajax im Zend Framework.................................................................................................... 118 5.4.1 Der Controller........................................................................................................ 119 5.4.2 Die View................................................................................................................ 120 Integration in eine Zend Framework-Applikation ............................................................... 121 5.5.1 Der Place-Controller.............................................................................................. 122 5.5.2 Das View-Skript mit HTML fürs Rating ergänzen ................................................ 124 5.5.3 JavaScript in die View-Skripte einbauen ............................................................... 125 5.5.4 Der Server-Code .................................................................................................... 128 Zusammenfassung ............................................................................................................... 130 Mit der Datenbank arbeiten................................................................................. 131 Datenbankabstraktion mit Zend_Db_Adapter ..................................................................... 131 6.1.1 Einen Zend_Db_Adapter erstellen......................................................................... 132 6.1.2 Die Datenbankabfrage ........................................................................................... 133 6.1.3 Einfügen, Aktualisieren und Löschen .................................................................... 134 6.1.4 Spezifische Unterschiede zwischen den Datenbanken........................................... 136 Tabellenabstraktion mit Zend_Db_Table ............................................................................ 136 6.2.1 Was ist das Table-Data-Gateway-Pattern?............................................................. 137 6.2.2 Die Arbeit mit Zend_Db_Table ............................................................................. 138 6.2.3 Einfügen und Aktualisieren mit Zend_Db_Table .................................................. 139 6.2.4 Einträge mit Zend_Db_Table löschen ................................................................... 141 Zend_Db_Table als Model .................................................................................................. 142 6.3.1 Das Model testen ................................................................................................... 144 6.3.2 Tabellenbeziehungen mit Zend_Db_Table ............................................................ 149 Zusammenfassung ............................................................................................................... 153 Benutzerauthentifizierung und Zugriffskontrolle.............................................. 155 Benutzerauthentifizierung und Zugriffskontrolle ................................................................ 155 7.1.1 Was ist Authentifizierung? .................................................................................... 156 7.1.2 Was ist Zugriffskontrolle? ..................................................................................... 156 Die Implementierung der Authentifizierung........................................................................ 157 7.2.1 Die Komponente Zend_Auth................................................................................. 157 7.2.2 Einloggen über die HTTP-Authentifizierung......................................................... 158
VII
Inhalt 7.3
7.4
7.5 8 8.1
8.2
8.3
8.4
8.5 9 9.1
9.2
9.3
9.4
VIII
Zend_Auth in einer echten Applikation ...............................................................................161 7.3.1 Das Einloggen ........................................................................................................161 7.3.2 Eine Begrüßungsnachricht in der View-Hilfsklasse ...............................................165 7.3.3 Das Ausloggen........................................................................................................166 Die Implementierung der Zugriffskontrolle .........................................................................167 7.4.1 Die Arbeit mit Zend_Acl ........................................................................................168 7.4.2 Die Konfiguration eines Zend_Acl-Objekts ...........................................................170 7.4.3 Das Zend_Acl-Objekt prüfen .................................................................................171 Zusammenfassung................................................................................................................175 Formulare ............................................................................................................. 177 Die Arbeit mit Zend_Form...................................................................................................178 8.1.1 Integrierte Datenfilter und Validatoren...................................................................178 8.1.2 Integrierte Fehlerbehandlung..................................................................................181 8.1.3 Dekoratoren zur Vereinfachung des Markups ........................................................181 8.1.4 Plug-in-Loader zur eigenen Anpassung..................................................................182 8.1.5 Internationalisierung...............................................................................................183 8.1.6 Unterformulare und Displaygroups ........................................................................183 Ein Login-Formular erstellen ...............................................................................................184 8.2.1 Pfade einrichten ......................................................................................................184 8.2.2 Das Formular-View-Skript .....................................................................................184 8.2.3 Aktualisierung der Controller-Action AuthController............................................185 8.2.4 Die Basisklasse für das Login-Formular.................................................................187 Filtern und Validieren ..........................................................................................................188 8.3.1 Einfaches Filtern und Validieren ............................................................................188 8.3.2 Eigene Fehlermeldungen ........................................................................................189 8.3.3 Die Internationalisierung des Formulars.................................................................190 8.3.4 Selbst erstellte Validatoren .....................................................................................192 Die Dekoration des Login-Formulars...................................................................................194 8.4.1 Standarddekoratoren von Zend_Form ....................................................................194 8.4.2 Eigene Dekoratoren setzen .....................................................................................194 Zusammenfassung................................................................................................................198 Suchfunktionen ................................................................................................... 199 Die Vorteile einer Suchfunktion...........................................................................................199 9.1.1 Zentrale Usability-Probleme der User ....................................................................200 9.1.2 Die Rangliste der Resultate ist wichtig...................................................................200 Die Komponente Zend_Search_Lucene ...............................................................................200 9.2.1 Einen separaten Suchindex für Ihre Website erstellen............................................201 9.2.2 Leistungsfähige Abfragen.......................................................................................203 9.2.3 Best Practices .........................................................................................................208 Eine Suchfunktion für Places...............................................................................................209 9.3.1 Indexaktualisierung bei neu eingefügten Inhalten ..................................................210 9.3.2 Erstellen des Suchformulars und Darstellung der Ergebnisse.................................219 Zusammenfassung................................................................................................................222
Inhalt 10 10.1
10.2
10.3
10.4
10.5 11 11.1
11.2
11.3
11.4 11.5
E-Mails .................................................................................................................. 223 Die Grundlagen von E-Mails............................................................................................... 223 10.1.1 E-Mails – einfach dargestellt ................................................................................. 224 10.1.2 Analyse einer E-Mail-Adresse ............................................................................... 225 Die Arbeit mit Zend_Mail ................................................................................................... 225 10.2.1 E-Mails mit Zend_Mail erstellen ........................................................................... 226 10.2.2 E-Mails mit Zend_Mail versenden ........................................................................ 227 Einen Support-Tracker für Places erstellen ......................................................................... 230 10.3.1 Die Applikation entwerfen..................................................................................... 230 10.3.2 Integration von Zend_Mail in die Applikation ...................................................... 234 10.3.3 Header in der Support-E-Mail einfügen................................................................. 236 10.3.4 Attachments an Support-E-Mails anhängen........................................................... 237 10.3.5 Formatierung der E-Mail ....................................................................................... 238 Lesen von E-Mails............................................................................................................... 241 10.4.1 Abholen und Speichern von E-Mails ..................................................................... 241 10.4.2 E-Mails mit der Applikation lesen ......................................................................... 242 Zusammenfassung ............................................................................................................... 246 Deployment .......................................................................................................... 247 Den Server einrichten .......................................................................................................... 247 11.1.1 Designen für verschiedene Umgebungen............................................................... 248 11.1.2 Die Arbeit mit virtuellen Hosts in der Entwicklung............................................... 251 Versionskontrolle mit Subversion ....................................................................................... 253 11.2.1 Erstellen des Subversion-Repositorys.................................................................... 253 11.2.2 Code aus dem Repository auschecken ................................................................... 254 11.2.3 Änderungen ins Repository committen.................................................................. 255 11.2.4 Aktualisierung einer lokalen Arbeitskopie............................................................. 256 11.2.5 Der Umgang mit Konflikten .................................................................................. 257 11.2.6 Eine saubere Kopie aus dem Repository holen...................................................... 259 11.2.7 Die Arbeit mit Branches ........................................................................................ 259 11.2.8 Externer Code ........................................................................................................ 260 Funktionale Tests ................................................................................................................ 260 11.3.1 Funktionales Testen mit Selenium IDE ................................................................. 261 11.3.2 Automatisierung der Selenium IDE-Tests ............................................................. 264 11.3.3 Funktionstests mit Zend_Http_Client .................................................................... 265 Das Skripting des Deployments........................................................................................... 267 Zusammenfassung ............................................................................................................... 268
Teil III – Machen Sie Ihre Applikation leistungsfähiger .............................................. 269 12 12.1
12.2
Der Austausch mit anderen Applikationen........................................................ 271 Die Integration von Applikationen ...................................................................................... 272 12.1.1 Austausch strukturierter Daten .............................................................................. 272 12.1.2 Produktion und Verarbeitung strukturierter Daten................................................. 273 12.1.3 Wie Webservices arbeiten...................................................................................... 274 12.1.4 Aufgabengebiete für Webservices ......................................................................... 275 Die Produktion und Verarbeitung von Feeds mit Zend_Feed.............................................. 276
IX
Inhalt
12.3
12.4
12.5 13 13.1
13.2
13.3
13.4
13.5 14 14.1 14.2 14.3
X
12.2.1 Die Produktion eines Feeds ....................................................................................276 12.2.2 Die Verarbeitung eines Feeds.................................................................................278 RPCs mit Zend_XmlRpc erstellen .......................................................................................279 12.3.1 Die Arbeit mit Zend_XmlRpc_Server ....................................................................280 12.3.2 Die Arbeit mit Zend_XmlRpc_Client.....................................................................288 Die Nutzung von REST-Webservices mit Zend_Rest..........................................................289 12.4.1 Was ist REST?........................................................................................................290 12.4.2 Die Arbeit mit Zend_Rest_Client ...........................................................................291 12.4.3 Die Arbeit mit Zend_Rest_Server ..........................................................................294 Zusammenfassung................................................................................................................296 Mashups mit öffentlichen Webservices............................................................. 297 Der Zugriff auf öffentliche Webservices ..............................................................................298 13.1.1 Zend_Gdata ............................................................................................................298 13.1.2 Zend_Service_Akismet ..........................................................................................300 13.1.3 Zend_Service_Amazon ..........................................................................................301 13.1.4 Zend_Service_Audioscrobbler ...............................................................................301 13.1.5 Zend_Service_Delicious.........................................................................................301 13.1.6 Zend_Service_Flickr ..............................................................................................302 13.1.7 Zend_Service_Gravatar ..........................................................................................302 13.1.8 Zend_Service_Nirvanix..........................................................................................302 13.1.9 Zend_Service_RememberTheMilk.........................................................................303 13.1.10 Zend_Service_Simpy..............................................................................................303 13.1.11 Zend_Service_SlideShare.......................................................................................303 13.1.12 Zend_Service_StrikeIron........................................................................................303 13.1.13 Zend_Service_Technorati.......................................................................................304 13.1.14 Zend_Service_Yahoo .............................................................................................304 Werbeanzeigen mit Amazon-Webservices darstellen ..........................................................305 13.2.1 Die Amazon-Model-Klasse ....................................................................................305 13.2.2 Die View-Hilfsklasse amazonAds ..........................................................................307 13.2.3 Die View-Hilfsklasse cachen..................................................................................308 Darstellen von Flickr-Bildern...............................................................................................311 13.3.1 Die Flickr-Model-Klasse ........................................................................................311 13.3.2 Flickr in einem Action-Controller verwenden ........................................................313 Mit Zend_Gdata auf Google zugreifen.................................................................................315 13.4.1 Die YouTube-API in einem Action-Controller ......................................................316 13.4.2 Die Seite für die Videokategorien ..........................................................................317 13.4.3 Die Seite mit den Videolisten .................................................................................318 13.4.4 Die Video-Seite ......................................................................................................320 Zusammenfassung................................................................................................................321 Das Caching beschleunigen............................................................................... 323 Die Vorteile des Cachings....................................................................................................324 Die Funktionsweise des Cachings ........................................................................................324 Die Implementierung von Zend_Cache................................................................................328 14.3.1 Die Zend_Cache-Frontends.....................................................................................329 14.3.2 Zend_Cache-Backends ............................................................................................340
Inhalt 14.4
14.5 14.6 15 15.1
15.2
15.3
15.4
Caching auf verschiedenen Ebenen der Applikation ........................................................... 342 14.4.1 Was in den Cache soll............................................................................................. 342 14.4.2 Optimale Verfallszeit des Caches ........................................................................... 342 Cache-Tags.......................................................................................................................... 343 Zusammenfassung ............................................................................................................... 344 Internationalisierung und Lokalisierung ........................................................... 345 Die Übersetzung in andere Sprachen und Idiome................................................................ 345 15.1.1 Die Übersetzung in andere Sprachen ..................................................................... 346 15.1.2 Die Übersetzung von Idiomen ............................................................................... 347 Die Arbeit mit Zend_Locale und Zend_Translate ............................................................... 347 15.2.1 Die Locales mit Zend_Locale setzen ..................................................................... 347 15.2.2 Übersetzung mit Zend_Translate ........................................................................... 349 Eine zweite Sprache für die Places-Applikation ................................................................. 351 15.3.1 Die Auswahl der Sprache ...................................................................................... 352 15.3.2 Das Front-Controller-Plug-in LanguageSetup ....................................................... 355 15.3.3 Die View übersetzen.............................................................................................. 357 15.3.4 Datum mit Zend_Locale korrekt darstellen ........................................................... 359 Zusammenfassung ............................................................................................................... 361
16 16.1
PDFs erstellen...................................................................................................... 363 Die Grundlagen von Zend_Pdf............................................................................................ 364 16.1.1 Erstellen oder Laden von Dokumenten.................................................................. 364 16.1.2 Seiten im PDF-Dokument erstellen ....................................................................... 364 16.1.3 Metainformationen im Dokument einfügen ........................................................... 365 16.1.4 Speichern des PDF-Dokuments ............................................................................. 367 16.2 Einen PDF-Berichtsgenerator erstellen ............................................................................... 367 16.2.1 Das Model für das Berichtsdokument.................................................................... 367 16.2.2 Das Model für die Berichtsseite............................................................................. 368 16.3 Text auf einer Seite zeichnen............................................................................................... 370 16.3.1 Die Wahl der Fonts................................................................................................ 370 16.3.2 Den Font setzen und Text einfügen ....................................................................... 370 16.3.3 Umbrochenen Text einfügen.................................................................................. 371 16.4 Die Arbeit mit Farben.......................................................................................................... 373 16.4.1 Die Wahl der Farben.............................................................................................. 373 16.4.2 Farben einstellen.................................................................................................... 373 16.5 Die Arbeit mit Styles ........................................................................................................... 374 16.6 Formen zeichnen ................................................................................................................. 375 16.6.1 Linien zeichnen...................................................................................................... 375 16.6.2 Gestrichelte Linien setzen...................................................................................... 375 16.6.3 Rechtecke und Polygone zeichnen......................................................................... 377 16.6.4 Das Zeichnen von Kreisen und Ellipsen ................................................................ 379 16.7 Objekte drehen .................................................................................................................... 380 16.8 Bilder auf der Seite einfügen ............................................................................................... 381 16.9 Objekte mit Schnittmasken zeichnen................................................................................... 382 16.10 Generierung von PDF-Berichten ......................................................................................... 382 16.11 Zusammenfassung ............................................................................................................... 384
XI
Inhalt A A.1 A.2 A.3 A.4 A.5
A.6 A.7 A.8 B B.1
B.2
B.3 B.4
B.5 C C.1
C.2
C.3 C.4
Die PHP-Syntax im Schnelldurchgang .............................................................. 385 PHP-Grundlagen ..................................................................................................................386 Variablen und Typen ............................................................................................................387 Strings ..................................................................................................................................388 Arrays...................................................................................................................................391 Bedingungen und Schleifen..................................................................................................392 A.5.1 Bedingungen ......................................................................................................392 A.5.2 Schleifen.................................................................................................................393 Alternative Syntax für den verschachtelten Block ...............................................................396 Funktionen ...........................................................................................................................396 Zusammenfassung................................................................................................................397 Objektorientiertes PHP........................................................................................ 399 Objektorientierung in PHP ...................................................................................................400 B.1.1 Klassen, Objekte und Vererbung............................................................................400 B.1.2 Erweitern von Klassen............................................................................................403 B.1.3 Abstrakte Klassen und Interfaces ...........................................................................403 B.1.4 Magische Methoden ...............................................................................................406 Die Standard PHP Library....................................................................................................409 B.2.1 Die Arbeit mit Iteratoren ........................................................................................409 B.2.2 Die Arbeit mit ArrayAccess und Countable ...........................................................411 PHP4 ....................................................................................................................................412 Software-Designpatterns ......................................................................................................412 B.4.1 Das Singleton-Designpattern ..................................................................................412 B.4.2 Das Registry-Designpattern....................................................................................414 Zusammenfassung................................................................................................................416 Tipps und Tricks.................................................................................................. 417 Tipps und Tricks für MVC...................................................................................................418 C.1.1 Module ...................................................................................................................418 C.1.2 Case Sensitivity ......................................................................................................421 C.1.3 Routing ...................................................................................................................423 Diagnostik mit Zend_Log und Zend_Debug........................................................................426 C.2.1 Zend_Debug ...........................................................................................................426 C.2.2 Zend_Log ...............................................................................................................426 Zend_Db_Profiler ................................................................................................................428 Zusammenfassung................................................................................................................431
Vorwort Aus kleinen Dingen können große Sachen werden. Im August 2006 beschloss ich, ein kurzes Einführungs-Tutorial für das Zend Framework (Version 0.1.4!) zu schreiben. Das haben viele Leute gelesen und meine Fehler korrigiert, was ich sehr nett fand. Im Oktober 2006 nahm Ben Ramsey über IRC mit mir Kontakt auf und fragte, ob ich daran interessiert sei, ein Buch über das Zend Framework zu schreiben. Offenbar hatte Chris Shiflett mich weiterempfohlen, nachdem er mein Tutorial und anderes, mit dem Zend Framework zusammenhängendes Material von mir gelesen hatte, und dachte, ich könnte schreiben. Das war ganz einfach ein Fehler, wirklich! Ben brachte mich mit Mike Stephens von Manning in Kontakt, und ich vereinbarte mit ihm, einen Entwurf für ein Buch über das Zend Framework zu erstellen. Er stellte mich Marjan Bace vor, die mit mir den Prozess für den Entwurf eines Buches durcharbeitete. Marjans freundliche Worte und ihre Ermunterungen halfen mir ganz außerordentlich. Als wir wussten, worum es in dem Buch gehen sollte, glaubte ich daran, dass ich es auch schreiben könnte, und begann im Januar 2007. Im Mai erkannten wir alle, dass ich nicht der Welt schnellster Schreiber bin, und wenn wir dieses Buch noch in diesem Jahrzehnt herausbringen wollten, dann brauchten wir Hilfe! Nick Lo antwortete auf diesen Hilferuf, und später kam noch Steven Brown dazu. Ihr Enthusiasmus hat dafür gesorgt, dass dieses Buch auch vollendet wurde. Es stellte sich heraus, dass Nick mit Grafiken viel besser umgehen kann als ich, und so sind die Diagramme wirklich schön anzusehen! Im Verlauf des Schreibens reifte das Zend Framework weiter, und die Releases 1.0, 1.5 und 1.6 erschienen. (Die Version 1.6 erschien spät im Entwicklungszyklus dieses Buches. Diese Version decken wir auch ab, aber nicht die neuen Dojo-Integrationsfeatures.) Wir konnten miterleben, wie das Zend Framework aus einer Sammlung von im Wesentlichen ungetestetem Code zu der ausgereiften, stabilen Code-Basis wuchs, die es heute bildet. Tausende von Entwicklern, darunter auch ich, nutzen das Zend Framework als die Basis, auf der sie ihre Websites und Applikationen aufbauen. Ich hoffe, dass Sie anhand dieses Buches nun auch in der Lage sind, sich in unsere Mitte zu gesellen. ROB ALLEN
1
Vorwort
Danksagungen Erst als wir uns tatsächlich an das Verfassen eines Buches machten, entdeckten wir, warum der Abschnitt mit den Danksagungen in jedem Buch erklärt, dass das Buch mehr als nur die Arbeit des Autors (oder der Autoren) ist, der bzw. die auf dem Buchumschlag erscheinen! Dementsprechend ist auch dieses Buch das Werk einer Gruppe, die nicht nur aus uns, also Rob, Nick und Steven besteht. Wir möchten uns bei allen bedanken, die uns dabei halfen, die Ursprungsidee in das Buch zu verwandeln, das Sie nun in Händen halten. Wir sind unseren Lektoren Joyce King, Douglas Pundick und Nermina Miller zu Dank verpflichtet, die uns während der Schreibphase mit Rat und Zuspruch zur Seite standen. Ohne euch hätten wir es einfach nicht geschafft. Nächtliche und frühmorgendliche SkypeChats konnten uns den Prozess, ein Buch zu schreiben, und die zur Vollendung eines Manuskripts erforderliche Organisation erhellen. Hinter den Kulissen von Manning hat noch eine Unmenge anderer Leute sehr intensiv daran gearbeitet, damit dieses Buch es bis in die Buchhandlungen schafft. Wir möchten uns bei ihnen allen bedanken, insbesondere unserer Herausgeberin Marjan Bace, unserem Mitherausgeber Mike Stephens, unserer Fachlektorin Karen Tegtmeyer und Megan Yockey, die bei dem Papierkram den Durchblick behielt. Als das Manuskript in die Produktionsphase eintrat, leistet uns Mary Piergies, unsere Projektlektorin, unschätzbare Hilfestellungen. Auch das restliche Produktionsteam mit Tony Roberts, Dottie Marsico, Tiffany Taylor, Leslie Haimes, Elizabeth Martin, Gabriel Dobrescu und Steven Hong war daran beteiligt, das Buch für Sie fertigzustellen. Wir sind auch all den Fachkorrektoren des Manuskripts sehr dankbar, die das Buch in den verschiedenen Phasen der Vollständigkeit durchgesehen haben: Deepak Vohra, Jonathon Bloomer, Horaci Macias, Jeff Watkins, Greg Donald, Peter Pavlovich, Pete Helgren, Marcus Baker, Edmon Begoli, Doug Warren, Thomas Weidner, Michal Minicki, Ralf Eggert, David Hanson, Andries Seutens und Dagfinn Reiersøl. Wir möchten uns auch ganz herzlich bedanken bei all jenen, die das Buch schon vorab bestellt und beim MEAP-Programm mitgemacht haben. Ihre Rückmeldungen haben das Buch viel besser gemacht, als es ohne Sie geworden wäre. Die Qualität und Lesbarkeit des Ihnen vorliegenden Texts ist so viel besser als unser anfänglicher Start – dank des bemerkenswerten Einsatzes von Andy Carroll, unserem Fachkorrektor. Dank seiner Arbeit sind die Texte nun viel prägnanter und einfacher zu lesen. Schließlich danken wir auch Matthew Weier O’Phinney, der für uns die technischen Inhalte überprüft hat. Er war sehr großzügig mit seiner Zeit und seinem Rat, obwohl natürlich alle Fehler und Auslassungen allein uns zuzuschreiben sind.
Rob Allen Ich möchte mich bei meiner Frau Georgina und meinen Söhnen dafür bedanken, dass sie dieses Projekt mit mir durchgestanden haben. Durch Georginas Unterstützung und Ermuti-
2
Vorwort gung konnte ich bis zum Ende durchhalten. Ich verspreche, kein neues Buch zu schreiben, bis wir mit der Renovierung fertig sind! Ich bedanke mich auch bei meinen Eltern, dass sie meinen weitschweifigen Ausführungen über etwas, das sie nicht verstehen, zugehört haben, und dass sie mir den Wunsch, Dinge bis zum Ende durchzuziehen, mitgegeben haben. Carl, mein Chef, und die anderen vom Team bei Big Room Internet verdienen ebenfalls meinen Dank, weil sie meinen Fortschrittsberichten zugehört haben und weil sie damit umgehen konnten, dass ich nicht so aufmerksam war, wie ich es hätte sein sollen, weil ich oft noch spät nachts oder früh morgens geschrieben habe. Ich bedanke mich auch bei den vielen Mitarbeitern am Zend Framework, die sich um den Code, die Dokumentation und weitere Hilfe bei Mailing-Listen, Foren und dem IRCChannel #zftalk gekümmert haben. Durch die Community ist das Zend Framework zu dem geworden, was es heute ist, und ich bin sehr glücklich dazuzugehören.
Nick Lo Mein erster Dank muss an Rob gehen, weil er mir die Möglichkeit gegeben hat, an diesem Projekt teilzunehmen, und für seine Geduld, als ich mich dabei durchgewurschtelt habe. Ich schließe mich ebenfalls dem Dank an unsere Lektoren an – es ist beeindruckend, was ein guter Lektor noch aus einem Text herausquetschen kann, nachdem man ihn schon zigmal durchgearbeitet hat. Mein persönlicher Dank geht an meine ganze Familie für die Begeisterung, die sie zu Anfang hatten, und die sie auch wieder haben werden, wenn ich ihnen die gedruckte Ausgabe in die Hand gebe. Es wird mich auch nicht stören, wenn ihre Augen glasig werden, sobald sie erkennen, dass das Buch für sie so interessant ist wie (um es mit den Worten meines Bruders zu sagen) eine Bedienungsanleitung für einen Kühlschrank! Dank gilt schließlich auch allen Entwicklerkollegen, die ihr Wissen der Entwicklergemeinschaft zur Verfügung gestellt haben. Es ist ermutigend zu sehen, wie hilfsbereit einander oft vollkommen Fremde sein können, die nicht mit einem offensichtlichen Vorteil rechnen können. Dieser Geist hat dem Buch auch wohlgetan mit Vorschlägen wie z.B. des MEAPLesers Antonio Ruiz Zwollo und seinen .htaccess-Settings, die wir in Kapitel 11 verwendet haben.
Steven Brown Ich bedanke mich ganz herzlich bei Michael Stephens, der die Empfehlung gab, dass ich Rob und Nick beim Schreiben dieses Buches helfe. Dank auch an Rob und Nick, dass ich an Bord kommen durfte und dass sie Verständnis dafür hatten, wenn persönliche Belange mich davon abhielten, soviel zum Buch beizutragen, wie ich gehofft hatte. Dank an Nermina Miller, die dafür gesorgt hat, dass meine Texte gut aussehen, und vielen Dank an Nick, dass meine Diagramme nun so gut aussehen.
3
Vorwort Doch vor allem danke ich meiner Frau Tamara, die immer noch darauf wartet, dass der Garten endlich fertig gestaltet wird, und die freundlich lächelt und nickt, wenn ich über Codeprobleme schwadroniere. Tamara war immer da, um mich in schweren Zeiten zu unterstützen, und die guten Zeiten noch schöner zu machen.
4
Über dieses Buch Im Jahre 2005 stellte Andi Gutmans von Zend Technologies das PHP Collaboration Project vor und gab somit den Startschuss für das Zend Framework. Im März 2006 kam der erste Code heraus, die Version 1.0 erschien im Juli 2007, und seitdem gab es regelmäßige Releases. Zend Framework bietet ein qualitativ hochwertiges Framework für PHP, das sich von den meisten anderen durch seine Philosophie unterscheidet, hier alles je nach Bedarf nutzen zu können. So wählen Entwickler selbst aus, welche Bestandteile sie daraus bei dem jeweiligen Projekt nutzen wollen. Dieses Buch zeigt, wie Sie das Zend Framework optimal einsetzen. Die jeweiligen Techniken werden am Beispiel einer Website demonstriert, die im Verlaufe des Buches entwickelt wird. Wir schauen uns die wichtigen Komponenten des Zend Frameworks an und zeigen, wie man sie jeweils im Kontext einer echten Applikation einsetzt. Somit ergänzt dieses Buch die funktionale Betrachtungsweise des Frameworks aus dem OnlineHandbuch, indem Ihnen gezeigt wird, wie alles zusammenpasst und wie Sie damit Websites und Applikationen von hoher Qualität produzieren.
Für wen ist dieses Buch gedacht? Dieses Buch richtet sich an PHP-Entwickler, die mit dem Zend Framework arbeiten wollen oder bereits damit arbeiten. Weil das Zend Framework einer bedarfsorientierten Philosophie folgt, werden nicht alle Kapitel für alle Leser sofort von Nutzen sein. Allerdings gehen wir davon aus, dass alle Leser aus jedem Kapitel etwas mitnehmen können, auch wenn Sie das Kapitel erneut zur Hand nehmen müssen, um die Einzelheiten nachzulesen, sobald Sie mit der jeweiligen Komponente arbeiten! Dies ist kein Anfängerbuch. Wir gehen davon aus, dass Sie mit PHP vertraut sind und die objektorientierte Programmierung verstanden haben. Anhang A, „Die PHP-Syntax im Schnelldurchgang“, und Anhang B, „Objektorientiertes PHP“, bieten eine hilfreiche Übersicht über die Grundlagen, aber sie sind kein Ersatz für ein Buch, das sich vollständig diesem Thema widmet.
5
Über dieses Buch
Weitere Informationen über das Zend Framework Neben diesem Buch ist die Website des Zend Frameworks unter http://framework.zend.com/ eine ausgezeichnete Informationsquelle. Das Online-Handbuch unter http://framework.zend. com/manual/de/ ist die definitive Referenzdokumentation für alle Komponenten des Frameworks. Für Hilfe, Anregungen und Diskussionen über das Zend Framework empfehlen wir, die Mailing-Listen zu abonnieren. Weitere Einzelheiten bekommen Sie unter http://framework.zend.com/wiki/display/ZFDEV/Contributing+to+Zend+Framework, und die Archive befinden sich unter http://framework.zend.com/archives. Schließlich gibt es einen interaktiven Echtzeit-Chat über das Zend Framework im IRC-Network Freenode im Channel #zf-talk.
Roadmap Dieses Buch ist in drei Teile unterteilt. Teil 1 stellt das Zend Framework vor und zeigt, wie man mit den Komponenten des Zend Frameworks eine einfache „Hello World“Anwendung implementiert. In Teil 2 schauen wir uns die Komponenten des Frameworks an, die für die meisten Webapplikationen nützlich sind, und Teil 3 stellt die weniger häufig eingesetzten Komponenten vor, die bei bestimmten Projekten ihren Einsatz finden. Die Grundlagen Kapitel 1 stellt vor, welche Komponenten das Zend Framework enthält, um die Erstellung von Websites schnell und effizient durchzuführen. Darin wird auch erläutert, warum wir überhaupt mit einem Framework arbeiten und welche Vorteile das bringt. In Kapitel 2 kommt dann der erste Code auf den Tisch. Wir beginnen langsam und erstellen die einfachste, vollständige Website, die anhand des Designpatterns Model-ViewController (MVC) möglich ist. Dieses Kapitel steckt den Rahmen ab und stellt die zentralen Konzepte über den Code und das Design des Zend Frameworks vor, die als Grundlage für die Teile 2 und 3 dienen. Eine Basisapplikation In Kapitel 3 entwickeln wir das anfängliche Design und den Code für Places to take the kids!, eine mit dem Zend Framework gestaltete reale Community-Website. Wir starten mit dem Bootstrapping und der Codeorganisation und bauen eins nach dem anderen zusammen bis zum Controller-Code der Homepage. Kapitel 4 erweitert die Arbeit aus Kapitel 3, um das Look & Feel des Website-Frontends zu entwickeln. Wir arbeiten mit den Komponenten Zend_View und Zend_Layout, um ein Composite-View-System zu entwickeln, das die für bestimmte Seiten spezifischen Darstellungselemente von denjenigen trennt, die alle Seiten gemeinsam haben.
6
Roadmap Kapitel 5 stellt die grundlegenden Prinzipien von Ajax vor und beschäftigt sich dann damit, wie man eine Ajax-Anfrage in eine Zend Framework MVC-Applikation integriert. Kapitel 6 untersucht die Interaktion mit einer Datenbank anhand der Datenbankkomponenten des Zend Frameworks, von der Datenbankabstraktion bis zur Tabellenabstraktion auf höherer Ebene. In Kapitel 7 geht es vor allem um die User, wie man ihren Zugriff authentifiziert und dann steuert, wie sie auf spezielle Abschnitte der Website zugreifen können. Kapitel 8 erklärt anhand der Komponente Zend_Form, wie man die Formulare auf Ihrer Website steuert. Kapitel 9 beschäftigt sich mit dem heiklen Thema der Suche auf der Website, damit Ihre User auf der Website das Gewünschte finden. Kapitel 10 erläutert die Komponente Zend_Mail, die das Versenden und Lesen von E-Mails erlaubt. Kapitel 11 schließt diesen Teil des Buches mit einem Blick auf Themen des Managements wie Versionskontrolle, Deployment und Testing ab. Machen Sie Ihre Applikation leistungsfähiger In Kapitel 12 werfen wir einen Blick auf die Integration von Webapplikationen und die XML-RPC- und REST-Protokolle. Sie erfahren überdies, wie man RSS- und Atom-Feeds in eine Applikation einbindet. Kapitel 13 erläutert, wie Sie mittels der Unmengen von Daten, die im Internet über öffentliche Webservices verfügbar sind, Ihre Website aufwerten. Kapitel 14 wirft einen Blick hinter die Kulissen und erklärt, wie man über das Caching eine Website beschleunigen und eine Applikation skalieren kann. In Kapitel 15 beschäftigen wir uns damit, wie man eine Website in verschiedenen Sprachen erstellt. Dabei werden auch die lokalen Idiome beachtet, die User auf einer vollendeten Website erwarten. Kapitel 16 stellt die Erstellung von PDF-Dokumenten mit Texten und Grafiken dar. Die Anhänge Anhang A nimmt Sie mit auf eine kurze Tour durch die PHP-Syntax und richtet sich vor allem an diejenigen, die von anderen Programmiersprachen her kommen. Anhang B beschreibt das Objektmodell von PHP5 und greift somit jenen unter die Arme, die vor der Arbeit mit dem Zend Framework vor allem prozedural programmiert haben. Anhang C enthält Tipps und Tricks, mit denen Sie Ihre Zend Framework-Applikationen einfacher entwickeln können.
7
Über dieses Buch
Codekonventionen und Downloads Der gesamte Quellcode des Buches wird in einer Schrift mit fester Breite wie dieser hier geschrieben und setzt sich somit vom Fließtext ab. Bei den meisten Listings
wird der Code kommentiert, um die zentralen Konzepte hervorzuheben. Manchmal finden sich anhand von nummerierten Punkten im Text zusätzliche Informationen über den Code. Wir haben uns bemüht, den Code so zu formatieren, dass er auf den verfügbaren Platz einer Buchseite passt, indem wir Zeilenumbrüche eingefügt haben und umsichtig mit Einrückungen umgegangen sind. Bei sehr langen Zeilen stehen manchmal auch Hinweise, dass die Zeile hier umbrochen wurde. Der Quellcode für alle Funktionsbeispiele steht zum Download unter http://www.manning. com/ZendFrameworkinAction bereit. Im Root-Ordner und auch in allen Kapitelordnern gibt es eine readme.txt-Datei, in der weitere Anweisungen zum Installieren und Starten des Codes zu finden sind. In Fällen, wo wir im Buch nicht alle Details ausführen konnten, enthält der begleitende Quellcode vollständig alle Einzelheiten. Sie brauchen eine funktionierende PHP-Installation auf dem Apache Webserver und eine MySQL-Datenbank, um die Beispiele starten zu können.
Author Online Durch den Kauf von Zend Framework im Einsatz haben Sie freien Zugriff auf ein englischsprachiges privates Webforum, das von Manning Publications geführt wird. Dort können Sie Kommentare über das Buch abgeben, technische Fragen stellen und sich bei den Autoren und anderen Anwendern Hilfe holen. Das Forum finden Sie unter www.manning.com/ZendFrameworkinAction oder www.manning.com/allen. Dort können Sie sich auch fürs Forum anmelden. Auf dieser Seite finden Sie Informationen, wie Sie nach der Registrierung ins Forum kommen, welche Art von Hilfe dort zur Verfügung steht, und die Verhaltensregeln dieses Forums. Über das vom Manning-Verlag bereitgestellte Forum finden Leser einen Platz, der informative Gespräche der Leser untereinander und zwischen Lesern und Autoren ermöglicht. Die Autoren sind in keiner Weise zu einer Mitarbeit verpflichtet, zumal deren Beitrag bei Author Online freiwillig (und unvergütet) bleibt. Wir schlagen vor, dass Sie versuchen, den Autoren ein paar anspruchsvolle Fragen zu stellen, auf dass ihr Interesse erhalten bleibe. Das Forum Author Online und die Archive der früheren Diskussionen werden auf der Website des Manning-Verlags so lange zugänglich sein, wie das Buch aufgelegt wird.
8
Über die Buchreihe
Über die Buchreihe Weil in den „… im Einsatz“-Büchern Einführungen, Übersichten und Schritt-für-SchrittAnleitungen enthalten sind, eigenen sie sich besonders gut zum Lernen und Nachschlagen. Den Forschungen der kognitiven Wissenschaften zufolge erinnert sich der Mensch am besten an jene Dinge, die er bei selbst motivierter Erforschung entdeckt. Obwohl es bei Manning keine kognitiven Wissenschaftler gibt, sind wir davon überzeugt, dass ein permanenter Lernerfolg erzielt wird, wenn das Lernen Phasen der Erforschung, des Spiels und interessanterweise auch des wiederholten Berichtens des jeweils Gelernten durchläuft. Menschen verstehen neue Dinge besser und können sie besser erinnern und also meistern, wenn sie sie aktiv erforschen. Menschen lernen, indem sie praktische Beispiele im Einsatz erleben. Ein wesentlicher Bestandteil eines „… im Einsatz“-Handbuchs ist, dass es sich an Beispielen orientiert. Damit wird der Leser ermutigt, Dinge auszuprobieren, mit neuem Code zu spielen und neue Konzepte und Ideen zu erforschen. Es gibt auch noch einen profaneren Grund für den Titel dieses Buches: Unsere Leser haben viel zu tun. Sie nutzen Bücher, um eine Aufgabe zu erledigen oder ein Problem zu lösen. Sie brauchen Bücher, in die sie ohne Umstände ein- und wieder aussteigen können und genau dann genau das lernen, was sie genau in diesem Moment brauchen. Sie brauchen Bücher, die sie im Einsatz begleiten. Die Bücher dieser Reihe sind speziell für solche Leser gestaltet.
9
Über die Buchreihe
Über die Buchreihe Weil in den „… im Einsatz“-Büchern Einführungen, Übersichten und Schritt-für-SchrittAnleitungen enthalten sind, eigenen sie sich besonders gut zum Lernen und Nachschlagen. Den Forschungen der kognitiven Wissenschaften zufolge erinnert sich der Mensch am besten an jene Dinge, die er bei selbst motivierter Erforschung entdeckt. Obwohl es bei Manning keine kognitiven Wissenschaftler gibt, sind wir davon überzeugt, dass ein permanenter Lernerfolg erzielt wird, wenn das Lernen Phasen der Erforschung, des Spiels und interessanterweise auch des wiederholten Berichtens des jeweils Gelernten durchläuft. Menschen verstehen neue Dinge besser und können sie besser erinnern und also meistern, wenn sie sie aktiv erforschen. Menschen lernen, indem sie praktische Beispiele im Einsatz erleben. Ein wesentlicher Bestandteil eines „… im Einsatz“-Handbuchs ist, dass es sich an Beispielen orientiert. Damit wird der Leser ermutigt, Dinge auszuprobieren, mit neuem Code zu spielen und neue Konzepte und Ideen zu erforschen. Es gibt auch noch einen profaneren Grund für den Titel dieses Buches: Unsere Leser haben viel zu tun. Sie nutzen Bücher, um eine Aufgabe zu erledigen oder ein Problem zu lösen. Sie brauchen Bücher, in die sie ohne Umstände ein- und wieder aussteigen können und genau dann genau das lernen, was sie genau in diesem Moment brauchen. Sie brauchen Bücher, die sie im Einsatz begleiten. Die Bücher dieser Reihe sind speziell für solche Leser gestaltet.
9
I Teil I – Die Grundlagen Die ersten beiden Kapitel dieses Buches bieten eine Einführung in das Zend Framework. Kapitel 1 führt aus, was das Zend Framework ist und was Sie damit in Ihrem PHPWebdevelopment-Prozess anfangen können. Im Kapitel 2 wird eine einfache Zend Framework-Applikation erstellt, über die Sie nachvollziehen können, wie die einzelnen Teile ineinander greifen. Mit der Vorstellung des MVC-Designpatterns (Model-View-Controller) strukturieren Sie Ihre Applikation, und dieses Designpattern ist auch die Grundlage für das restliche Buch.
1 1
Das Zend Framework – Eine Einführung
Die Themen dieses Kapitels
Warum Sie mit dem Zend Framework arbeiten sollten Was das Zend Framework kann Die Philosophie hinter dem Zend Framework Seit über zehn Jahren werden mit PHP dynamische Websites entwickelt. Anfänglich waren alle PHP-Websites als PHP-Code geschrieben, in den das HTML auf der gleichen Seite eingestreut war. Das hat sehr gut funktioniert, weil sofort eine Rückmeldung verfügbar ist, und bei einfachen Skripten hat das auch ausgereicht. In den Versionen 3 und 4 wurde PHP immer beliebter, und somit war es unausweichlich, dass auch immer größere Applikationen mit PHP geschrieben werden. Es wurde schnell klar, dass das Mischen von PHP- und HTML-Code keine langfristige Lösung für große Websites ist. Die Probleme sind im Rückblick offensichtlich: Die Wartbarkeit und die Erweiterbarkeit werden problematisch. Zwar bekommt man extrem schnell Ergebnisse, wenn man PHP mit HTML mischt, aber auf lange Sicht wird es sehr aufwendig und schwierig, die Website zu aktualisieren. Ein wichtiger Aspekt des Publizierens im Internet ist, dass es dynamisch ist und die Inhalte und das Seitenlayout sich immer wieder ändern können – ein wirklich cooles Feature! Große Websites ändern sich dauernd, und das Look & Feel der meisten Sites wird regelmäßig aktualisiert, wenn sich die Bedürfnisse von Usern (und Werbekunden!) ändern. Da musste etwas passieren. Das Zend Framework wurde kreiert, um sicherzustellen, dass Produktion und Pflege von auf PHP basierenden Websites auch auf lange Sicht einfach erfolgen kann. Es enthält ein umfassendes Set von wiederverwertbaren Komponenten, darunter alles Mögliche von einem Komponentenset für MVC-Applikationen (Model-View-Controller) bis hin zu Klassen zur PDF-Generierung. Im Laufe dieses Buches werden wir uns anschauen, wie man mit all diesen Komponenten des Zend Frameworks im Kontext einer realen Website arbeitet.
13
1 Das Zend Framework – Eine Einführung In diesem Kapitel führen wir aus, was das Zend Framework ist und warum Sie damit arbeiten sollten, und wir schauen uns auch die grundlegende Designphilosophie an. Diese Einführung in das Framework dient als Anleitung für den Rest des Buches und hilft dabei, die Designentscheidungen hinter jeder Komponente klarer zu machen. Beginnen wir nun damit, wie man über das Zend Framework die Codebasis einer Website strukturieren kann.
1.1
Die Struktur von PHP-Websites Die Lösung für das verworrene Durcheinander von PHP- und HTML-Code auf einer Website liegt in der Strukturierung. Der grundlegendste Ansatz, um Applikationen innerhalb von PHP-Sites zu strukturieren, ist die Trennung der Aufgabenbereiche (separation of concerns). Das bedeutet, dass sich der für die Darstellung zuständige Code nicht in der gleichen Datei befinden sollte wie der Code, der die Verbindung mit der Datenbank aufbaut und die Daten sammelt. Neulinge vermischen üblicherweise die beiden Codearten (Abbildung 1.1).
HTML-Kopfzeile
PHP-Datenbankverbindung
HTML-Seitenbeginn
PHP-Datenbankabfrage
HTML und PHP-Tabelle
HTML-Fußzeile
Abbildung 1.1 Wenn ein Anfänger eine PHP-Datei erstellt, ist der HTML- und PHP-Code meist auf lineare Weise vermischt.
Die meisten Entwickler beginnen von sich aus mit der Einführung von Struktur in den Code einer Website, und so kommt Leben in das Konzept der Wiederverwendbarkeit auf. Üblicherweise sieht das so aus, dass der Code, der sich um die Verbindung mit der Datenbank kümmert, separat in einer Datei mit einem Namen wie db.inc.php abgelegt wird. Wenn man den Datenbankcode getrennt führt, scheint es nur logisch zu sein, auch den Code auszulagern, der die auf jeder Seite vorkommenden Header- und Footer-Elemente darstellt. Dann werden Funktionen eingeführt, um das Problem der globalen Variablen zu
14
1.1 Die Struktur von PHP-Websites lösen, die sich gegenseitig beeinflussen, indem man darauf achtet, dass die Variablen nur im Geltungsbereich ihrer jeweils eigenen Funktion vorkommen. Wenn die Website wächst, werden die allgemeinen, auf mehreren Seiten vorkommenden Funktionalitäten in Libraries gruppiert. Bevor Sie es richtig gemerkt haben, ist die Applikation viel einfacher zu pflegen, und es wird deutlich leichter, neue Features einzubauen, ohne dass der vorhandene Code beschädigt wird. Die Website wächst dann weiter, bis sie an den Punkt kommt, wo der unterstützende Code so groß ist, dass Sie es nicht mehr schaffen, die gesamte Funktionsweise der Site auf einmal im Kopf zu behalten. Wir PHP-Coder sind es gewöhnt, auf den Schultern von Riesen zu stehen, weil unsere Sprache den einfachen Zugriff auf Libraries wie der GD Library, die vielen Libraries für den Datenbank-Client-Zugriff und sogar systemspezifische Libraries wie COM auf Microsoft Windows ermöglicht. Es war unausweichlich, dass das objektorientierte Programmieren (OOP) die PHP-Landschaft betreten musste. In PHP4 gab es schon eingeschränkte OOP-Features, aber PHP5 enthält einen ausgezeichneten Support für all die Dinge, die Sie in einer objektorientierten Sprache erwarten. Da gibt es neben Sichtbarkeitsspezifikatoren für Klassenelemente (public, private und protected) auch Schnittstellen und abstrakte Klassen sowie die Unterstützung für Exceptions.
Abbildung 1.2 Eine typische MVCApplikation trennt den Code einer Applikation in verschiedene Aufgabenbereiche.
Die verbesserte Unterstützung der Objektorientierung durch PHP erlaubt die Schaffung komplizierterer Libraries (die man auch als Frameworks bezeichnet) wie Zend Framework, die das Designpattern Model-View-Controller unterstützen – eine Möglichkeit, wie man die Dateien von Webapplikationen strukturieren kann. Dieses Designpattern wird in Abbildung 1.2 gezeigt. Eine Applikation, die anhand der MVC-Prinzipien designt wird, enthält letzten Endes mehr Dateien, aber jede Datei ist auf ihre jeweils eigene Aufgabe spezialisiert, was die Pflege deutlich vereinfacht. Beispielsweise wird der gesamte Code für Datenbankabfragen in als Models bezeichneten Klassen gespeichert. Der eigentliche HTML-Code wird als View bezeichnet (der auch einfache PHP-Logik enthalten kann), und die ControllerDateien kümmern sich um die Verbindung des korrekten Models mit den korrekten Views, um die gewünschte Seite darzustellen.
15
1 Das Zend Framework – Eine Einführung Das Zend Framework ist nicht die einzige Möglichkeit, die Ihnen zur Organisation einer Website basierend auf den MVC-Prinzipien zur Verfügung steht – in der Welt von PHP gibt es noch viele andere. Schauen wir uns nun an, was das Zend Framework enthält und warum Sie die Arbeit mit diesem Framework in Erwägung ziehen sollten.
1.2
Gründe für das Zend Framework Bevor wir in die Verwendung des Zend Frameworks einsteigen, wollen wir uns zuerst anschauen, warum wir gerade dieses PHP-Framework allen anderen vorziehen. Kurz gesagt wird mit dem Zend Framework ein standardisiertes Set von Komponenten eingeführt, mit dem man auf einfache Weise Webapplikationen entwickeln kann, die leicht weiterentwickelt, gepflegt und erweitert werden können. Das Zend Framework besitzt eine Reihe zentraler Features, die zu untersuchen sich lohnt: Alles ist gleich out of the box vorhanden. Es hat ein modernes Design. Es ist einfach zu erlernen. Es ist vollständig dokumentiert. Die Entwicklung ist einfach. Die Entwicklung geht schnell. Diese Liste ist recht nüchtern und knapp. Also sehen wir uns jeden Aspekt im Einzelnen an und schauen, was das für uns Webentwickler bedeutet.
1.2.1
Alles ist gleich out of the box vorhanden
Das Zend Framework ist ein umfassendes, lose gekoppeltes Framework, in dem alles enthalten ist, was Sie zur Entwicklung Ihrer Applikation benötigen. Dazu gehört eine robuste MVC-Komponente, damit Ihre Website auch wirklich den Best Practices entsprechend strukturiert ist, und neben weiteren, eher esoterisch anmutenden Elementen auch andere Komponenten zur Authentifizierung, für Suchfunktionen, Lokalisierung, PDF-Erstellung, E-Mail und die Verbindung mit Webservices. Diese Komponenten können in die sechs in Abbildung 1.3 gezeigten Kategorien gruppiert werden. Das heißt aber nicht, dass das Zend Framework nicht auch prima mit anderen Libraries klar kommt – das macht es sehr wohl. Ein zentrales Feature des Designs dieses Frameworks ist, dass Sie ganz einfach nur genau die Bestandteile bei Ihrer Applikation oder mit anderen Libraries wie PEAR, der ORM-Datenbank-Library Doctrine oder der TemplateEngine Smarty nutzen können. Sie können Komponenten aus dem Zend Framework sogar bei anderen PHP-MVC-Frameworks wie Symfony, CakePHP oder CodeIgniter einsetzen.
16
1.2 Gründe für das Zend Framework
1.2.2
MVC
Authentifizierung und Zugriff
Internationalisierung
Applikationsübergreifende Kommunikation
Webservices
Core
Abbildung 1.3 Im Zend Framework gibt es viele Komponenten, doch wir können sie zur einfacheren Übersicht in diese sechs Kategorien gruppieren.
Modernes Design
Das Zend Framework ist unter Verwendung moderner Designtechniken, die man als Designpattern bezeichnet, in objektorientiertem PHP5 geschrieben. Software-Designpattern betrachtet man als High-Level-Lösungen für Designprobleme, und somit sind sie keine spezifischen Implementierungen dieser Lösungen. Die eigentliche Implementierung hängt von der Natur des restlichen Designs ab. Das Zend Framework verwendet viele Designpattern, die sorgfältig und umsichtig implementiert wurden, um die maximale Flexibilität für die Applikationsentwickler zu bieten, ohne sie mit zuviel Arbeit zu belasten! Das Framework versteht den PHP-Weg und zwingt Sie nicht, alle Komponenten zu verwenden – Sie können diese frei wählen und beliebig zusammenstellen. Das ist vor allem deswegen wichtig, weil Sie damit spezielle Komponenten in eine vorhandene Site einführen können. Das zentrale Konzept hierbei ist, dass jede Komponente innerhalb des Frameworks nur wenige Abhängigkeiten von anderen Komponenten hat. Somit können Sie in Ihr aktuelles Projekt spezielle Zend Framework-Komponenten wie Zend_Search, Zend_Pdf oder Zend_Cache einführen, ohne den ganzen restlichen Projektcode ersetzen zu müssen.
1.2.3
Leicht zu erlernen
Wenn es Ihnen wie uns ergeht, dann wissen Sie, wie schwer es ist zu lernen, wie ein Riesenberg an Code funktioniert. Zum Glück ist das Zend Framework modular aufgebaut. Also kann jeder Entwickler, der einer bedarfsorientierten Designphilosophie anhängt, das Framework leicht und schrittweise erlernen. Die einzelnen Komponenten hängen nicht von vielen anderen Komponenten ab und sind somit einfach durchzuarbeiten. Das Design jeder Komponente ist so aufgebaut, dass Sie nicht verstanden haben müssen, wie es in seiner Gesamtheit funktioniert, bevor Sie es verwenden und davon profitieren können. Wenn Sie erst einmal etwas Erfahrung mit der einen Komponente gemacht haben, ist es recht unkompliziert, die fortgeschrittenen Features zu erlernen, weil das alles schrittweise erfolgen kann. Das senkt die Einstiegsschwelle. Die Konfigurationskomponente Zend_Config wird beispielsweise verwendet, um eine objektorientierte Schnittstelle für die Konfigurationsdatei zu bieten. Sie unterstützt die
17
1 Das Zend Framework – Eine Einführung beiden fortgeschrittenen Features Sektionsüberladung und verschachtelte Schlüssel, doch diese Features müssen nicht erst begriffen werden, um mit der Komponente arbeiten zu können. Wenn der Entwickler erst einmal eine funktionierende Implementierung von Zend_Config in seinem Code hat, wird er selbstsicherer, und die Nutzung der fortgeschrittenen Features ist nur noch ein kleiner Schritt.
1.2.4
Vollständige Dokumentation
Egal wie gut der Code auch ist, eine fehlende Dokumentation kann ein Projekt durch mangelnde Verinnerlichung zum Scheitern bringen. Weil sich das Zend Framework an Entwickler richtet, die sich zur Erledigung ihrer Arbeit nicht durch Berge von Quellcode wühlen wollen, ist im Zend Framework die Dokumentation mit dem Code gleichgestellt. Das bedeutet, dass das Kernteam keinen neuen Code im Framework zulässt, wenn es keine begleitende Dokumentation gibt. Das Framework unterstützt zwei Arten von Dokumentationen: API und Enduser. Die APIDokumentation wird über phpDocumenter erstellt und wird mittels spezieller DocBlockKommentare im Quellcode automatisch generiert. Diese Kommentare finden sich üblicherweise direkt über jeder Klasse, Funktion und Deklaration einer Elemenvariablen. Ein zentraler Vorteil der Nutzung von DocBlocks ist, dass IDEs wie das Eclipse-PDT-Projekt oder Studio von Zend in der Lage sind, beim Coding mit Tools zur Autovervollständigung zu arbeiten, was die Produktivität eines Entwicklers verbessert. Die Dokumentation ist enthalten Das Zend Framework besitzt ein vollständiges Manual zum Download (http:// framework.zend.com/manual/de). In diesem Manual finden sich detaillierte Einzelheiten über alle Komponenten des Frameworks und darüber, welche Funktionalität verfügbar ist. Durch Beispiele erfahren Sie schnell, wie Sie mit dieser Komponente in einer Applikation arbeiten können. Wichtiger noch ist, dass im Falle der komplizierteren Komponenten (wie z. B. dem Zend_Controller) auch die theoretischen Grundlagen des Vorgangs erläutert werden, damit Sie verstehen, wie und warum die Komponente funktioniert. Die zum Framework gehörige Dokumentation erklärt nicht, wie alle Komponenten zusammengehören, um eine vollständige Applikation zu erstellen. Infolgedessen sind aus der Community eine Reihe von Tutorials im Netz erschienen, mit denen Entwickler ihren Einstieg ins Framework finden können. Diese sind im Wiki für das Zend Framework unter http://framework.zend.com/wiki/x/q zusammengestellt. Die Tutorials sind zwar ein hilfreicher Ausgangspunkt, gehen bei den Komponenten allerdings nicht in die Tiefe und zeigen auch nicht, wie man damit im Rahmen einer nicht trivialen Applikation arbeitet. Doch genau dafür ist dieses Buch geschrieben worden.
18
1.2 Gründe für das Zend Framework
1.2.5
Einfachere Entwicklung
Wie bereits angemerkt, ist eine der Stärken von PHP, dass damit die Entwicklung von einfachen, dynamischen Webseiten sehr einfach ist. Diese einfache Nutzbarkeit hat dazu geführt, dass Millionen fantastische Websites entstehen konnten, die es sonst nicht gegeben hätte. Als Folge davon treffen wir bei PHP-Programmierern auf eine große Bandbreite von Anfängern, die es als Hobby betreiben, bis hin zu Entwicklern, die für große Unternehmen tätig sind. Das Zend Framework ist so gestaltet, dass Entwickler aller Stufen damit einfacher und schneller arbeiten können. Was vereinfacht nun die Entwicklung? Das zentrale Feature des Framework ist, dass es zu getestetem, verlässlichen Code führt, der die Routinearbeit einer Applikation erledigt. Das bedeutet, dass Sie nur den Code schreiben müssen, den Sie für die Applikation brauchen. Um den Code, der die langweiligen Aufgaben übernimmt, hat man sich schon gekümmert – damit brauchen Sie Ihren eigenen Code nicht belasten.
1.2.6
Schnelle Entwicklung
Mit dem Zend Framework bekommen Sie Ihre Webapplikation schneller an den Start oder können neue Features bei einer aktiven Website besser einbauen. Im Framework sind viele der zugrunde liegenden Komponenten einer Applikation enthalten, damit Sie sich auf die zentralen Bestandteile Ihrer Applikation konzentrieren können. Sie können sich gleich an einen bestimmten Bestandteil einer Funktionalität machen und sehen sofort die Ergebnisse. Das Framework beschleunigt die Entwicklung außerdem dadurch, dass die Standardeinstellung der meisten Komponenten der Normalfall ist. Anders gesagt müssen Sie sich nicht durch viele Konfigurationseinstellungen für jede Komponente arbeiten, bloß damit Sie sie überhaupt einsetzen können. Die einfachste Nutzung des gesamten MVC ist beispielsweise in diesem folgenden kurzen Code enthalten: require_once('Zend/Loader.php'); Zend_Loader::registerAutoload(); Zend_Controller_Front::run('/Pfad/zum/Controller');
Wenn das erst einmal läuft, ist das Hinzufügen einer neuen Seite in Ihrer Anwendung so einfach wie das Einsetzen einer neuen Funktion in einer Klasse – so wie eine neue ViewSkript-Datei im korrekten Verzeichnis. Entsprechend bietet Zend_Session eine Vielzahl von konfigurierbaren Optionen, damit Sie Ihre Session nach Belieben managen können. Aber um die Komponente in den meisten Use Cases zu verwenden, müssen diese Optionen nicht erst vorher eingestellt werden.
1.2.7
Strukturierter, leicht zu pflegender Code
Wie wir gesehen haben, sorgt die Trennung der verschiedenen Verantwortlichkeiten für eine strukturierte Applikation. Das bedeutet auch, dass Sie beim Fixen von Bugs leichter finden, wonach Sie suchen. Wenn Sie entsprechend in den Display-Code ein neues Feature
19
1 Das Zend Framework – Eine Einführung einbauen, gehören die einzigen Dateien, die Sie sich dafür ansehen müssen, zur DisplayLogik. Das hilft, Bugs zu vermeiden, die beim Einbau des neuen Features entstehen könnten. Das Framework erleichtert auch das Schreiben von objektorientiertem Code, was die Pflege Ihrer Applikation deutlich vereinfacht. Wir haben uns nun angeschaut, warum das Zend Framework entwickelt wurde, und die zentralen Vorteile vorgestellt, die es bei der Entwicklung von PHP-Websites und –Applikationen bringt. Nun wenden wir uns den im Zend Framework enthaltenen Komponenten zu und wie sie uns dabei helfen, unsere Websites leichter zu erstellen.
1.3
Was ist das Zend Framework? Das Zend Framework ist eine PHP-Rahmen-Library zur Erstellung von PHP-Webapplikationen. Die Komponenten greifen ineinander und bilden so ein komplett ausgestattetes Framework mit allen Komponenten, die für moderne, leicht zu erstellende und einfach zu pflegende Applikationen erforderlich sind. Diese recht einfache Beschreibung ist allerdings nicht die ganze Geschichte. Also schauen wir uns hier an, woher dieses Framework kommt und was es normalerweise enthält.
1.3.1
Woher stammt das Framework?
Frameworks gibt es schon viele Jahre. Das allererste Web-Framework, mit dem Rob in einem echten Projekt gearbeitet hat, war Fusebox, das ursprünglich für ColdFusion geschrieben worden war. Seitdem sind schon viele andere Frameworks erschienen, wobei der nächste wichtige Höhepunkt das in Java geschriebene Struts ist. Von Struts wurden ein paar PHP-Clones geschrieben, doch der Code ließ sich nicht gut auf PHP übertragen. Das größte Problem bestand darin, dass Java-Webapplikationen in einer virtuellen Maschine laufen, die fortwährend läuft, und somit ist die Startup-Zeit der Webapplikation nicht für jede Anfrage ein Faktor. PHP initialisiert jeden Request von Grund auf, und somit wurden die Struts-Clones durch die erforderliche große Initialisierung relativ langsam. Vor einigen Jahren kam ein neues Framework namens Rails auf die Welt, das auf einer relativ unbekannten Sprache namens Ruby beruht. Rails (auch bekannt als Ruby on Rails) vertrat das Konzept der Konvention über Konfiguration (convention over configuration) und hat die Webentwicklung im Sturm erobert. Kurz nach Erscheinen von Rails betraten mehrere direkte PHP-Clones die Szene, zusammen mit ein paar Frameworks, die von Rails eher inspiriert als eine unmittelbare Kopie davon waren.
Abbildung 1.4 Das Zend Framework verfügt über eine Menge Komponenten, die alles enthalten, was man für eine Enterprise-Applikation benötigt.
Ende 2005 begann Zend Technologies (ein auf PHP spezialisiertes Unternehmen) mit dem Zend Framework als Teil seines Projekts PHP Collaboration, um die Nutzung von PHP voranzutreiben. Zend Framework ist ein Open-Source-Projekt, das ein Webframework für PHP bietet. Es ist als Standard-Framework gedacht, auf dem zukünftige PHP-Applikationen basieren sollen.
1.3.2
Was ist darin enthalten?
Das Zend Framework besteht aus vielen, voneinander abgegrenzten Komponenten, die man in sechs Kategorien gruppieren kann. Als vollständiges und umfassendes Framework enthält es alles, was Sie für die Erstellung von professionellen Webapplikationen benötigen. Trotzdem ist das System sehr flexibel und wurde so entworfen, dass Sie sich nach Belieben daraus bedienen können, um sich die für Ihre Situation erforderlichen Teile herauszupicken. In Anlehnung an den Überblick aus Abbildung 1.3 listet die Abbildung 1.4 die wichtigsten Komponenten aus allen Kategorien des Frameworks auf. Jede Komponente des Frameworks enthält verschiedene Klassen, darunter auch die Hauptklasse, nach der die Komponente ihren Namen erhalten hat. Die Komponente Zend_Config enthält neben der Klasse Zend_Config auch die Klassen Zend_Config_Ini und Zend_Config_Xml. Jede Komponente enthält außerdem eine Reihe anderer Klassen, die in Abbildung 1.4 nicht aufgeführt sind. Wir werden diese Klassen im Laufe des Buches erläutern, wenn wir uns mit den einzelnen Komponenten befassen.
21
1 Das Zend Framework – Eine Einführung 1.3.2.1
Die MVC-Komponenten
Die MVC-Komponenten bilden ein umfassendes MVC-System zur Erstellung von Applikationen, in denen die View-Templates von der Business-Logik und den ControllerDateien getrennt sind. Das MVC-System besteht aus Zend_Controller (dem Controller) und Zend_View (der View), wobei die Klassen Zend_Db und Zend_Service das Modell bilden. Abbildung 1.5 zeigt anhand von Zend_Db als Modell die Grundlagen des MVCSystems des Zend Frameworks. Die Klassenfamilie der Zend_Controller bietet ein Front-Controller-Designpattern, das Requests an Controller-Actions (auch Befehle genannt) weiterleitet, sodass die gesamte Verarbeitung zentralisiert wird. Wie man es von einem vollständig ausgestatteten System erwarten sollte, unterstützt der Controller Plug-ins für alle Phasen des Prozesses und hat integrierte Anschlusspunkte, damit Sie spezielle Teile des Verhaltens ändern können, ohne zuviel Arbeit investieren zu müssen. Das View-Skript-System heißt Zend_View und besteht aus einem auf PHP basierenden Template-System. Das bedeutet, dass anders als bei Smarty oder PHPTAL alle ViewSkripte in PHP geschrieben sind. Zend_View stellt ein Plug-in-Hilfssystem bereit, mit dem wiederverwendbarer Displaycode erstellt werden kann. Es ist so gestaltet, dass spezielle Anforderungen überschrieben werden können oder dass man auch mit einem ganz anderen Template-System wie z. B. Smarty arbeiten kann. Mit dem Zend_View arbeitet das Zend_Layout zusammen, mit dem man mehrere View-Skripte zusammenstellen kann, um die gesamte Webseite zu erstellen. Request des Browsers
Bootstrap-Datei: index.php
Zend_Controller_Front (Front-Controller) Zend_Controller_Router_Rewrite (Wählt die Action aus)
Model konkrete Instanz(en) von Zend_Controller_Action
(z. B. Zend_Db_Table-Instanz(en))
Nutzt verschiedene Zend_Action_Helper-Klassen
Zend_View (View)
Zend_Controller_Response_Http
Response an den Browser
22
Baut Darstellung auf
Nutzt verschiedene Zend_Action_Helper-Klassen
Abbildung 1.5 Der MVC-Ablauf in einer Zend Framework-Applikation arbeitet mit einem FrontController, um den Request zu verarbeiten und an einen speziellen Action-Controller zu delegieren. Dieser formt anhand von Models und Views die Response.
1.3 Was ist das Zend Framework? implementiert ein Table-Data-Gateway-Pattern, mit dem man neben den Webservices-Komponenten die Basis des Models innerhalb des MVC-Systems formen kann. Das Model enthält die Business-Logik für die Applikation, die in einer Webapplikation meist, aber nicht immer auf einer Datenbank basiert. Zend_Db_Table arbeitet mit Zend_Db, das einen objektorientierten, von Datenbanken unabhängigen Zugriff auf verschiedene Datenbanken wie z. B. MySQL, PostgreSQL, SQL Server, Oracle und SQLite bietet. Zend_Db_Table
Das simpelste Setup der MVC-Komponenten erledigt man mit diesem Code: require_once 'Zend/Controller/Front.php'; Zend_Controller_Front::run('/Pfad/zu/Ihren/Controllern');
Es ist allerdings wahrscheinlicher, dass bei einer nicht trivialen Applikation eine kompliziertere Bootstrap-Datei erforderlich ist. Das werden wir in Kapitel 2 untersuchen, wenn wir mit dem Zend Framework eine vollständige Applikation erstellen. Die MVC-Klassen arbeiten in Kombination mit einigen der Kernklassen, die die Basis einer vollständigen Applikation bilden. Beim Framework selbst muss nichts konfiguriert werden, aber bei Ihrer Applikation ist höchstwahrscheinlich eine gewisse Konfiguration erforderlich (z. B. die Details des Datenbank-Logins). Mit Zend_Config kann eine Anwendung Konfigurationsdaten aus PHP-Arrays oder INI- oder XML-Dateien lesen. Auch ein hilfreiches Vererbungssystem ist darin enthalten, um verschiedene Konfigurationseinstellungen auf unterschiedlichen Servern zu unterstützen, z. B. für Produktions-, Stagingund Test-Server. Jeder PHP-Entwickler, der sein Geld wert ist, legt Wert auf die Sicherheit. Die Validierung eingegebener Daten und deren Filterung ist der Schlüssel zu einer sicheren Applikation. Zend_Filter und Zend_Validate helfen dem Entwickler, dass die eingegebenen Daten sicher in der Applikation verwendet werden können. Die Klasse Zend_Filter enthält ein Filterset, das unerwünschte Daten aus der Eingabe entfernt oder umwandelt, wenn sie den Filter durchlaufen. Ein numerischer Filter entfernt beispielsweise alle Zeichen aus dem Input, die keine Ziffern sind, und ein HTML-EntitiesFilter wandelt das Zeichen „<“ in die Sequenz „<“ um. Man kann entsprechende Filter einrichten, damit die Daten für den jeweiligen Kontext auch wirklich valide sind, in dem sie verwendet werden. Zend_Validate enthält eine ähnliche Funktion wie Zend_Filter, außer dass darin auch eine Ja/Nein-Antwort auf die Frage „Sind diese Daten so, wie ich sie erwarte?“ gegeben wird. Generell stellt man mittels der Validierung, sicher, dass die Daten die korrekte Form haben, also um z. B. zu gewährleisten, dass die in einem E-Mail-Adressen-Feld eingegebenen Daten tatsächlich auch eine E-Mail-Adresse sind. Wenn die Validierung nicht positiv ausfällt, kann Zend_Validate auch eine Nachricht ausgeben, in der darauf hingewiesen wird, warum der Input nicht valide ist, damit der Enduser die entsprechenden Fehlermeldungen bekommt.
23
1 Das Zend Framework – Eine Einführung 1.3.2.2
Authentifizierungs- und Zugriffskomponenten
Nicht bei allen Applikationen müssen User identifiziert werden, doch es ist eine überraschend weit verbreitete Anforderung. Authentifizierung nennt man den Prozess der Identifizierung eines Users, meist anhand eines Tokens wie einem Username-Passwort-Paar, doch könnte es genauso gut auch ein Fingerabdruck sein. Die Zugriffssteuerung ist der Prozess der Entscheidung, ob ein authentifizierter User die Erlaubnis bekommt, auf eine bestimmte Ressource zuzugreifen und damit zu operieren, z. B. einen Datenbankeintrag. Weil das zwei separate Prozesse sind, enthält Zend Framework auch zwei getrennte Komponenten: Zend_Acl und Zend_Auth. Mit Zend_Auth wird der User identifiziert, und das wird normalerweise zusammen mit Zend_Session verwendet, das diese Information über mehrere Seiten-Requests speichern kann (das nennt man Token-Persistenz). Zend_Acl wiederum nutzt dieses Authentifizierungs-Token, um anhand eines rollenbasierten Zugriffkontrollsystems (role-based access control, RBACL) den Zugriff auf private Informationen zu gewähren. Flexibilität ist ein zentrales Designziel innerhalb der Zend_Auth-Komponente. Es gibt so viele Wege, um einen User zu authentifizieren, dass das Zend_Auth-System mit der Absicht erstellt wurde, dass der User seine eigene Methode erstellt, wenn keine der angebotenen Lösungen passend ist. Authentifizierungsadapter gibt es für HTTP-Digest, Datenbanktabellen, OpenID, InfoCard und LDAP. Für jede andere Methode müssen Sie eine Klasse erstellen, mit der Zend_Auth_Adapter erweitert wird. Zum Glück ist das nicht schwierig, wie wir in Kapitel 7 sehen werden. Weil Zend_Acl eine Implementierung eines RBACL-Systems ist, beschreibt das Manual diese Komponente in abstrakten Begriffen. RBACL ist ein generisches System, das den Zugriff auf alles Mögliche durch beliebige Personen bieten kann, also ist von spezifischen Begrifflichkeiten abzuraten. Von daher sprechen wir von Rollen (roles), die den Zugriff auf Ressourcen (resources) anfragen. Eine Rolle kann alles sein, was Zugriff auf etwas haben will, das unter dem Schutz des Zend_Acl-Systems steht. Generell heißt das bei einer Webapplikation, dass eine Rolle eine User-Gruppe ist, die als Nutzer von Zend_Auth identifiziert wird. Eine Ressource ist alles, was geschützt werden muss. Das ist meist ein Eintrag in einer Datenbank, kann aber auch eine auf der Festplatte gespeicherte Bilddatei sein. Weil es so vielfältige Ressourcen geben kann, versetzt uns das Zend_Acl-System in die Lage, ganz einfach eigene zu erstellen, indem wir innerhalb das Zend_Acl_Role_ Interface bei unserer Klasse implementieren. 1.3.2.3
Komponenten für die Internationalisierung
Wir leben in einer multikulturellen Welt mit vielen verschiedenen Sprachen. Das Zend Framework enthält dafür zur Lokalisierung eine reichhaltige Funktionalität, damit Sie Ihre Applikationen auf die User-Zielgruppe anpassen können. Das umfasst kleinere Aufgabenstellungen wie das für die Sprache korrekte Währungssymbol und auch größere wie die, den gesamten Text auf einer Seite in die korrekte Sprache zu übersetzen. Datums- und Zeitroutinen stehen darüber hinaus als einfaches, objektorientiertes Interface zur Verfügung und auch Einstellungen für die vielen Wege, wie ein Kalender dargestellt werden kann.
24
1.3 Was ist das Zend Framework? Das Zend Framework enthält die Klasse Zend_Locale, die neben Zend_Currency und Zend_Measure dafür zuständig ist, dass die korrekte Sprache und die richtigen Idiome verwendet werden. Die Komponente Zend_Translate ist mit der eigentlichen Übersetzung des Texts einer Website in die gewünschte Sprache betraut. 1.3.2.4
Komponenten für die Kommunikation zwischen den Applikationen
Im Zend Framework können Sie mit der Komponente Zend_Http_Client ganz einfach Daten aus anderen Websites und Services auslesen und sie dann auf Ihrer Site präsentieren. Diese Komponente funktioniert sehr ähnlich wie die PHP-Extension cURL, ist aber in PHP implementiert und kann bei solchen Situationen verwendet werden, wo cURL nicht aktiviert ist. Wenn Sie über HTTP mit einer anderen Applikation kommunizieren müssen, ist das üblichste Transferformat eine von zwei Spielarten von XML: XML-RPC und SOAP. PHP5 enthält einen ausgezeichneten SOAP-Support, und im Zend Framework kümmert sich der Zend_XmlRpc_Client um die leichte Verarbeitung von XML-RPC. Seit neuestem ist das leichtgewichtige JSON-Protokoll (JavaScript Object Notation) immer beliebter geworden, vor allem deswegen, weil es so einfach innerhalb des JavaScripts einer Ajax-Applikation verarbeitet werden kann. Zend_Json bietet eine schöne Lösung zum Erstellen und auch Lesen von JSON-Daten. 1.3.2.5
Komponenten für Webservices
Das Zend Framework enthält eine reichhaltige Funktionalität, um auf Dienste von anderen Anbietern zugreifen zu können. Diese Komponenten decken neben spezifischen Komponenten für die Arbeit mit den öffentlichen APIs von Google, Yahoo! und Amazon auch generische RSS-Feeds ab. RSS hat sich aus seiner Nische bei den technologischer orientierten Bloggern aufgemacht und wird nun auf der Mehrheit der News-Sites eingesetzt. Mit Zend_Feed bekommen Sie ein konsistentes Interface, um Feeds in den verschiedenen RSSund Atom-Versionen auf dem Markt zu lesen, ohne sich um die Details kümmern zu müssen. Google, Yahoo!, Amazon und andere Websites stellen für ihre Online-Dienste öffentliche APIs bereit, damit Entwickler um den Basisdienst herum weitere Applikationen erstellen können. Bei Amazon erlaubt die API den Zugriff auf die Daten von Amazon.com in der Hoffnung, dass die neue Applikation den Umsatz steigert. Entsprechend bietet Yahoo! den API-Zugriff auf seine Flickr-Fotodaten, um weitere Dienste für Flickr-User zu bieten, z. B. den Druck-Dienst von moo.com. Die traditionellen Yahoo!-Eigenschaften wie Suche, Nachrichten und Bilder stehen auch zur Verfügung. Das Zend Framework gruppiert diese und viele weitere Komponenten in einen Satz Klassen mit dem Präfix Zend_Service. Das sind Zend_Service_Amazon, Zend_Service_Delicious, Zend_Service_Simpy, Zend_ Service_SlideShare und Zend_Service_Yahoo, um nur einige aus dieser Familie zu nennen.
25
1 Das Zend Framework – Eine Einführung Google hat eine Reihe von Online-Applikationen mit der Möglichkeit des API-Zugriffs, die von der Komponente Zend_Gdata unterstützt werden. Mit Zend_Gdata können Sie auf die Google-Applikationen Blogger, Kalender, Base, YouTube und Code-Suche zugreifen. Um eine Konsistenz zu gewährleisten, stellt die Komponente Zend_Gdata die Daten anhand von Zend_Feed bereit. Wenn Sie also einen RSS-Feed verarbeiten können, können Sie auch Daten des Google-Kalenders verarbeiten. 1.3.2.6
Basiskomponenten
Im Zend Framework gibt es noch eine Reihe anderen Komponenten, die nicht so einfach in eine Kategorie zu stecken sind, und die wir deswegen in die Hauptkategorie gruppieren. Zu diesem Potpourri der Komponenten gehören Klassen für das Caching, die Suche und die Erstellung von PDFs. Die eher esoterische Measurement-Klasse findet sich ebenfalls in dieser Kategorie. Alle wollen möglichst schnelle Websites, und Caching ist ein Tool, mit dem Sie den Betrieb Ihrer Website beschleunigen können. Die Komponente Zend_Cache ist zwar nicht sonderlich sexy, aber enthält ein generisches und konsistentes Interface zum Caching von Daten aus einer Vielzahl von Backend-Systemen wie Festplatten, Datenbanken oder gar dem Shared Memory von APC. Durch diese Flexibilität können Sie mit Zend_Cache ganz klein anfangen, und wenn die Last auf Ihrer Site größer wird, kann die Caching-Lösung ebenfalls wachsen, damit Sie Ihre Server-Hardware optimal ausreizen können. Alle modernen Websites enthalten Suchfunktionen, doch die meisten sind so schrecklich, dass User der Site diese lieber über Google durchsuchen. Zend_Search_Lucene basiert auf der Suchmaschine Apache Lucene für Java. Sie enthält ein professionelles Textsuchsystem, mit dem Ihre User genau das Gesuchte finden können. Wie es sich für ein gutes Suchprogramm gehört, unterstützt Zend_Search_Lucene eine nach Rang orientierte Suche, sodass die besten Resultate sich ganz oben befinden, sowie ein leistungsfähiges Abfragesystem. Eine weitere Basiskomponente ist Zend_Pdf, mit der PDF-Dateien aus dem Programm heraus erstellt werden können. PDF ist ein besonders gut portierbares Format für zum Drucken vorgesehene Dokumente. Sie können die Position aller Elemente auf der Seite pixelgenau steuern, ohne sich darum kümmern zu müssen, wie unterschiedliche Browser die Seite darstellen. Zend_Pdf ist vollständig in PHP geschrieben und kann neue PDFDokumente erstellen oder vorhandene zur Bearbeitung laden. Das Zend Framework enthält auch eine gute E-Mail-Komponente namens Zend_Mail, um E-Mails in reinem Textformat oder als HTML verschicken zu können. So wie bei allen Komponenten des Zend Frameworks liegt die Betonung auf Flexibilität und sinnvolle Standardeinstellungen. In der Welt der E-Mails bedeutet das, dass man mit dieser Komponente E-Mails per SMTP oder über den Standard-PHP-Befehl mail() verschicken kann. Weitere Versandmöglichkeiten können einfach ins System gesteckt werden, indem eine neue Klasse geschrieben wird, die das Zend_Mail_Transport_Interface implementiert. Beim Versand einer E-Mail kommt ein objektorientiertes Interface zum Einsatz:
26
1.4 Die Designphilosophie des Zend Frameworks $mail = new Zend_Mail(); $mail->setBodyText('Meine erste E-Mail!') ->setBodyHtml('Meine erste E-Mail!') ->setFrom('[email protected]', 'Rob Allen') ->addTo('[email protected]', 'Ein Empfaenger') ->setSubject('Hallo von Zend Framework in Action!') ->send();
Dieses Snippet zeigt auch den Einsatz von Fluent Interfaces: Jede Elementfunktion gibt eine Objektinstanz zurück, sodass die Funktionen verkettet werden können, damit der Code einfacher zu lesen ist. Bei den Klassen des Zend Frameworks werden allgemein Fluent Interfaces eingesetzt, damit die Klassen leichter verwendbar sind. Sie werden es sicher zu schätzen wissen, dass das Zend Framework einen umfassenden Satz Komponenten enthält, mit denen der Großteil der Grundlagen einer Website erstellt werden können. Im Verlaufe des weiteren Wachstums des Frameworks werden noch mehr Komponenten ergänzt, und alle Komponenten folgen der Designphilosophie des Zend Frameworks, das die Qualität und Konsistenz des Frameworks sicherstellt.
1.4
Die Designphilosophie des Zend Frameworks Das Zend Framework hat sich eine Reihe von Zielen auf die Fahne geschrieben, die zusammen die Designphilosophie des Frameworks bilden. Wenn diese Ziele nicht zu Ihrer Sichtweise bei der Entwicklung von PHP-Applikationen passen, wird das Zend Framework wahrscheinlich nicht gut zu der Art und Weise passen, wie Sie die Dinge anpacken. Diese Ziele treffen für alle Komponenten des Frameworks zu, was sicherstellt, dass es auch zukünftig von Nutzen sein wird.
1.4.1
Komponenten von hoher Qualität
Der gesamte Code innerhalb der Library des Zend Frameworks ist von hoher Qualität. Das bedeutet, dass es die Features von PHP5 nutzt und keine Nachrichten des PHP-Parsers generieren wird (d. h. es ist also E_STRICT-konform). Das bedeutet, dass alle Nachrichten des PHP-Parsers in Ihren Logs aus Ihrem Code stammen und nicht aus dem Framework, was das Debugging beträchtlich vereinfacht! Das Zend Framework definiert hohe Qualität auch damit, dass die Dokumentation enthalten sein muss. Somit ist das Manual für eine bestimmte Komponente genauso wichtig wie der Code. Das Framework ist so konzipiert, dass ganze Applikationen ohne Abhängigkeiten von externen Libraries entwickelt werden können (außer Sie wollen das so). Das bedeutet, dass das Zend Framework viel eher ein komplettes Framework ist (wie Ruby on Rails oder Django Python) als eine Gruppe zusammenhangloser Komponenten, obwohl deutlich lockerer gekoppelt. Das gewährleistet eine Konsistenz bei den Komponenten: wie sie benannt werden, wie sie arbeiten und wie die Dateien in den Unterverzeichnissen angelegt werden. Darüber hinaus sollte auch noch erwähnt werden, dass das Zend Framework modular ist und nur wenige Abhängigkeiten zwischen den Modulen bestehen. Damit wird
27
1 Das Zend Framework – Eine Einführung sichergestellt, dass es gut mit anderen Frameworks und Libraries zusammenarbeitet und dass Sie sich beliebig in größerem oder kleinerem Umfang daraus bedienen können. Wenn Sie beispielsweise nur die PDF-Erstellung wollen, brauchen Sie nicht mit dem MVCSystem zu arbeiten.
1.4.2
Pragmatismus und Flexibilität
Ein weiteres Designziel für das Framework ist die Parole „Verändere kein PHP“. Der PHP-Weg ist einfach und enthält pragmatische Lösungen, und das Zend Framework ist so gedacht, diesem zu entsprechen und eine einfache Lösung für den Großteil der Entwickler zu bieten. Es ist aber auch leistungsfähig genug, um eine spezielle Verwendung mittels Extensions zu ermöglichen. Die Kernentwickler haben sehr gute Arbeit geleistet, indem sie die üblichen Szenarien abgedeckt und „Anschlusspunkte“ eingebaut haben, damit alle, die ein leistungsfähigeres oder spezialisiertes Verhalten benötigen, das Standardverhalten ganz einfach verändern können.
1.4.3
Saubere Klärung der Rechte auf geistiges Eigentum
Jeder, der zum Zend Framework beiträgt, hat eine Lizenzvereinbarung für Beiträge unterschrieben (Contributor License Agreement, CLA). Mit dieser Vereinbarung mit Zend wird der Status des Beitrages dieser geistigen Leistung sichergestellt. Das heißt, dass die Mitarbeiter bestätigen, dass sie (nach bestem Wissen) dazu berechtigt sind, diesen Beitrag zu leisten, und dass keinerlei Rechte auf das geistige Eigentum anderer beeinträchtigt werden. Das ist dazu gedacht, alle Nutzer des Frameworks vor potenziellen juristischen Problemen zu schützen, die mit der Nutzung geistigen Eigentums und dem Copyright einhergehen. Das Risiko ist minimal, doch das Verfahren, das von SCO gegen AutoZone angestrengt wurde, zeigte, dass mit einer Anzeige rechnen kann, wer möglicherweise das Copyright verletzenden Code nutzt. Wie bei allem ist es besser, gut vorbereitet zu sein. Der Quellcode des Zend Frameworks wird unter der neuen BSD-Lizenz lizenziert. Somit haben die User viele Freiheiten, den Code in unterschiedlichsten Applikationen zu verwenden: von Open-Source-Projekten bis zu kommerziellen Produkten. In der Kombination mit der sauberen Klärung der Anforderungen für das geistige Eigentum ist das Zend Framework gut positioniert, um von allen möglichen Leuten für alle mögliche Zwecke verwendet zu werden.
1.4.4
Support von Zend Technologies
Eine offensichtliche, aber wichtige Überlegung ist, dass das Zend Framework von Zend Technologies unterstützt wird. Das bedeutet, dass das Framework wahrscheinlich nicht vom Markt verschwinden wird, wenn die Kernentwickler inaktiv sind oder es nicht auf die neueste und beste Version von PHP aktualisiert wird. Zend Technologies verfügt auch über die Ressourcen, um Entwickler in Vollzeit an das Projekt zu setzen, damit es in einem kontinuierlichen Tempo weiterentwickelt wird.
28
1.5 Alternative PHP-Frameworks Wir haben nun einige Aspekte des Zend Frameworks abgedeckt, uns angeschaut, warum es existiert, was es enthält und seine allgemeinen Ziele vorgestellt. Eine Menge PHPFrameworks sind für die Ansprüche verschiedener Programmierer geeignet. Also sollten wir uns anschauen, wie das Zend Framework im Vergleich zu anderen Frameworks steht.
1.5
Alternative PHP-Frameworks Wenn man bedenkt, dass die Nutzung von PHP eine solch große Bandbreite hat, kann ein einziges Framework niemals passend für alle sein. Es gibt viele andere Frameworks, die in der Welt von PHP um Ihre Aufmerksamkeit buhlen, und alle haben ihre Stärken und Schwächen. In Tabelle 1.1 werden einige dieser Stärken und Schwächen aufgeführt. Wir haben recht willkürlich vier Frameworks ausgewählt, die in der Community alle auf gewisse Weise bekannt sind, aber damit ist auf keinen Fall gemeint, dass es die einzige Wahlmöglichkeit ist. Tabelle 1.1: Zentrale Features des Zend Frameworks, von CakePHP, CodeIgniter, Solar und Symfony Feature
Zend Framework
CakePHP
CodeIgniter
Solar
Symfony
Setzt alle Vorteile von PHP5 ein
Ja
Nein
Nein
Ja
Ja
Vorgeschriebene Verzeichnisstruktur
Nein (Optionale empfohlene Struktur)
Ja
Ja
Nein
Ja
Offizieller Support für Internationalisierung
Ja
In Arbeit für Version 1.2
Nein
Ja
Ja
Einrichtung des Frameworks über Befehlszeilen-Skripts erforderlich
Nein
Nein
Nein
Nein
Ja
Konfiguration erforderlich Ja (kleiner Aufwand)
Nein
Nein
Ja
Ja
Umfassende ORM enthalten
Ja
Nein
Nein
Ja (Propel)
Gute Dokumentation und Ja Tutorials
Ja
Ja
Ja
Ja
Unit-Tests für Quellcode verfügbar
Ja
Nein
Nein
Ja
Ja
Community-Support
Ja
Ja
Ja
Ja (ein wenig)
Ja
Lizenz
New BSD
MIT
Im Stil von BSD
New BSD
MIT
Nein
29
1 Das Zend Framework – Eine Einführung In diesem Buch geht es zwar um das Zend Framework, doch es lohnt sich, auch den anderen mal einen Blick zu schenken, um zu sehen, ob diese Ihren Anforderungen besser entsprechen. Wenn Sie immer noch kompatibel zu PHP4 sein wollen, müssen Sie entweder CakePHP oder CodeIgniter nehmen, weil die anderen PHP4 nicht unterstützen. Heutzutage wird es allerdings Zeit, sich von PHP4 zu verabschieden, weil es offiziell nicht weiterentwickelt wird.
1.6
Zusammenfassung In diesem Kapitel haben wir uns angeschaut, was das Zend Framework ist und warum es zum Schreiben von Webapplikationen sehr praktisch ist. Es ermöglicht die schnelle Entwicklung von Enterprise-Applikationen, weil es ein umfassendes Set von Komponenten bereitstellt und dabei mit den Best Practices des objektorientierten Designs arbeitet. Das Framework enthält sehr viele Komponenten: vom MVC-Controller über einen PDFGenerator bis hin zu einem leistungsfähigen Tool für Suchfunktionen. Das Zend Framework verfügt über eine Reihe von Designprinzipien, mit dem die hohe Qualität des Codes sichergestellt wird. Es ist außerdem gut dokumentiert. Für alle Beiträge gibt es ein CLA, und somit wird das Risiko von Problemen mit dem geistigen Eigentumsrecht mit dem Framework minimiert. Weil Zend Technologies sich verpflichtet hat, das Framework zu pflegen, können wir auch langfristig sicher sein, dass auf dieser Technologie aufbauende Applikationen Bestand haben werden. In diesem Buch geht es um Beispiele aus der realen Welt. Also werden wir an geeigneten Stellen die Ajax-Technologie einbauen. Gehen wir nun über zum zentralen Thema des Buches und schauen uns in Kapitel 2 an, wie man mit dem Zend Framework eine einfache, aber vollständige Applikation erstellt.
30
2 2
Hello Zend Framework!
Die Themen dieses Kapitels
Einführung in das Designpattern Model-View-Controller Die Controller-Komponenten des Zend Frameworks Die Komponente Zend_View Datenbanken als Models Bevor wir eingehend all die Komponenten des Zend Frameworks untersuchen können, müssen wir uns erst einmal orientieren, und das geht am einfachsten, indem wir eine einfache Website erstellen, die mit den MVC-Komponenten arbeitet. Bei einer Standard-PHPApplikation besteht der Code, der den Text „Hello World“ ausgibt, aus nur einer Zeile in einer Datei:
In diesem Kapitel erstellen wir mit dem Zend Framework eine HelloWorld-Applikation. Wir werden uns auch anschauen, wie man die Dateien einer Website auf der Festplatte organisiert, damit wir auch immer finden, was wir brauchen und suchen. Außerdem wird es darum gehen, welche Dateien das Zend Framework benötigt, um eine Applikation zu erstellen, die mit dem MVC-Designpattern arbeitet. Hinweis
Das Zend Framework benötigt viele Dateien, um die Basis zu schaffen, auf der eine vollständige Website erstellt werden kann. Das bedeutet, dass der Code unserer HelloWorld-Applikation unnötig langatmig erscheinen mag, wenn wir den Boden für die umfassende Website legen, die in den späteren Kapiteln folgen wird.
In diesem Kapitel gehen wir alle Dateien durch, die für die Erstellung von HelloWorld nötig sind. Wir werden auch das MVC-Design des Zend Frameworks und die Kernkomponenten erläutern, die es für die Erstellung von Controller, Views und Model unserer Applikation bietet. Steigen wir also gleich ein und schauen uns an, worum es bei diesem Designpattern Model-View-Controller eigentlich geht.
31
2 Hello Zend Framework!
2.1
Das Designpattern Model-View-Controller Damit Sie bei einer mit dem Zend Framework erstellten Applikation durchsteigen, müssen wir erst ein wenig Theorie behandeln. Das Zend Framework bietet eine Implementierung des Software-Designpatterns Model-View-Controller, das mit einem anderen Designpattern namens Front-Controller gekoppelt ist. Ein Software-Designpattern ist eine standardmäßige, generelle Lösung eines häufig vorkommenden Problems. Das bedeutet, dass die Implementierungen vielleicht variieren können, aber die Konzepte, mit denen die Probleme anhand eines bestimmten Patterns (Muster) gelöst werden, sind stets gleich. Das Pattern Front-Controller ist ein Mechanismus, mit dem der Einstiegspunkt in Ihre Applikation zentralisiert wird. Der Front-Controller-Handler (meist ist das index.php) akzeptiert alle Serveranfragen und startet innerhalb des Action-Befehls die korrekte Action-Funktion. Dieser Prozess wird als Routing und Dispatching bezeichnet. Das Zend Framework implementiert das Front-Controller-Pattern anhand verschiedener Subkomponenten. Die wichtigen sind der router und der dispatcher. Der router bestimmt, welche Action gestartet werden muss, dann führt der dispatcher die gewünschte Action aus und alle anderen Actions, die ggf. noch erforderlich sind. Das MVC-Pattern beschreibt einen Weg, um die zentralen Bestandteile einer Applikation in drei Hauptbereiche zu separieren: Model, View und Controller. Diese Abschnitte müssen Sie schreiben, um eine Applikation zu erstellen. In Abbildung 2.1 wird gezeigt, wie der Router und Dispatcher des Front-Controllers an Model, Controller und View angehängt werden, um eine Antwort (Response) auf die Anfrage (Request) eines Browsers zu produzieren. Innerhalb der MVC-Implementierung des Zend Framework finden wir fünf wichtige Bereiche vor, die unserer Aufmerksamkeit bedürfen. Der Router und der Dispatcher arbeiten zusammen, um festzulegen, welcher Controller basierend auf den Inhalten des URLs gestartet werden soll. Der Controller arbeitet mit dem Model und dem View zusammen, um die endgültige Webseite zu erstellen, die dann an den Browser zurückgeschickt wird. Schauen wir uns Model, View und Controller einmal genauer an. Anfrage des Browsers
Router
Dispatcher Model Controller View Antwort an den Browser
32
Abbildung 2.1 Der Front-Controller des Zend Frameworks arbeitet mit den MVC-Komponenten zusammen, um eine Webseite bereitzustellen. Der Router und der Dispatcher finden den korrekten Controller, der die Seite in Verbindung mit Model und View erstellt.
2.1 Das Designpattern Model-View-Controller
2.1.1
Das Model
Der Model-Teil des MVC-Patterns ist die ganze Business-Logik, die in der Applikation hinter den Kulissen arbeitet. Dies ist der Code, der entscheidet, wie die Versandkosten bei einer Online-Bestellung berechnet werden müssen, oder der weiß, dass ein Anwender einen Vor- und einen Nachnamen hat. Das Auslesen und Speichern von Daten aus einer Datenbank gehört auch zur Model-Schicht. In den Begrifflichkeiten des Codes heißt das: Das Zend Framework enthält die Komponenten Zend_Db_Table und Zend_Service. Zend_Db_Table bietet den Datenbankzugriff auf Tabellenebene und erlaubt die einfache Manipulation der von der Applikation verwendeten Daten. Zend_Service enthält eine Komponentensuite zum einfachen Zugriff sowohl auf öffentliche als auch private Webservices und deren Integration in Ihre Applikation.
2.1.2
Die View
Die View ist die Display-Logik der Applikation. Bei einer Webapplikation ist das normalerweise der HTML-Code, aus dem die Webseiten bestehen, aber er kann auch XML enthalten, das z. B. für einen RSS-Feed verwendet wird. Auch wenn die Website den Export in das CSV-Format (comma-separated values, kommagetrennte Werte) erlaubt, gehört die Generierung der CSV-Daten zur View. Die View-Dateien nennt man Templates oder Skripts, weil sie normalerweise Code enthalten, der die vom Model erstellten Daten darstellen kann. Es ist auch hilfreich, den komplexeren, auf Templates bezogenen Code in Funktionen zu verschieben, die man auch als View-Hilfsklassen (view helpers) bezeichnet. Das verbessert die Wiederverwendbarkeit des Codes. Standardmäßig verwendet die View-Klasse (Zend_View) des Zend Frameworks innerhalb der Skript-Dateien PHP; das kann aber auch durch eine andere Template-Engine wie Smarty oder PHPTAL ersetzt werden.
2.1.3
Der Controller
Der Controller ist der restliche Code, aus dem die Applikation besteht. Bei Webapplikationen legt der Controller-Code fest, was als Reaktion auf eine Webanfrage gemacht werden soll. Wie bereits erläutert, basiert das Controller-System des Zend Frameworks auf dem Designpattern Front-Controller, das über einen Handler (Zend_Controller_Front) die gemeinsam zusammenarbeitenden Action-Befehle (Zend_Controller_Action) dispatcht. Die Dispatcher-Komponente erlaubt, dass mehrere Action-Befehle innerhalb einer einzelnen Anfrage verarbeitet werden, was für flexiblere Applikationsarchitekturen sorgt. Die Action-Klasse ist für eine Gruppe damit zusammenhängender Action-Funktionen verantwortlich, die die eigentliche, durch die Anfrage erforderlich gewordene Arbeit leistet. Innerhalb des Front-Controllers von Zend Framework ist es möglich, beim Dispatchen mehrerer Actions ein einziges Anfrageresultat zu haben.
33
2 Hello Zend Framework! Nachdem wir nun ein wenig davon verstanden haben, wie das Zend Framework MVC implementiert, können wir uns darum kümmern, wie die Dateien im Einzelnen zusammenpassen. Wie bereits erwähnt, gibt es in einer Zend Framework-Applikation viele Dateien, und diese müssen wir in Verzeichnisse strukturieren.
2.2
Die Anatomie einer Zend Framework-Applikation Eine typische Zend Framework-Applikation besitzt viele Verzeichnisse. Damit ist gewährleistet, dass die unterschiedlichen Teile der Applikation getrennt bleiben. Die Top-LevelVerzeichnisstruktur wird in Abbildung 2.2 gezeigt.
Abbildung 2.2 Das Verzeichnis einer typischen Zend Framework-Applikation gruppiert die Dateien nach ihrer Rolle in der Applikation. So finden Sie ganz leicht die gesuchte Datei.
Es gibt im Ordner einer Applikation vier Top-Level-Verzeichnisse: application library public tests Die Verzeichnisse application, library und public werden verwendet, wenn die Anfrage eines Anwenders verarbeitet wird. Im Verzeichnis tests werden Unit-Test-Dateien gespeichert, über die Sie sicherstellen können, dass Ihr Code korrekt funktioniert.
2.2.1
Das Verzeichnis application
Dieses Verzeichnis enthält den gesamten Code, der für die Ausführung der Applikation erforderlich ist. Der Webserver hat keinen direkten Zugriff darauf. Um der Trennung zwischen Business-, Display- und Steuerungslogik mehr Nachdruck zu verschaffen, gibt es
34
2.2 Die Anatomie einer Zend Framework-Applikation drei separate Verzeichnisse innerhalb des application-Verzeichnisses, in denen die Dateien für das Model, die View und den Controller enthalten sind. Andere Verzeichnisse können je nach Bedarf erstellt werden, z. B. für Konfigurationsdateien.
2.2.2
Das Verzeichnis library
Alle Applikationen verwenden library-Code, weil alle bereits geschriebenen Code gerne wiederverwenden! In einer Zend Framework-Applikation wird das Framework selbst naheliegenderweise im library-Verzeichnis gespeichert. Allerdings kann man auch mit anderen Libraries wie einem angepassten Superset des Frameworks, einer Datenbank-ORM-Library wie Propel oder einer Template-Engine wie Smarty arbeiten. Libraries können überall gespeichert werden, wo die Applikation sie finden kann – entweder in einem globalen oder einem lokalen Verzeichnis. Ein globales include-Verzeichnis ist ein solches, auf das alle PHP-Applikationen auf dem Server zugreifen können, z. B. /usr/php_include (oder c:\code\php_include für Windows). Es wird über die Einstellung include_path in der Konfigurationsdatei php.ini gesetzt. Alternativ kann jede Applikation ihre Libraries lokal innerhalb des Verzeichnisses der Applikation speichern. In einer Zend Framework-Applikation nehmen wir ein Verzeichnis namens library, obwohl es auch nicht unüblich ist, dieses Verzeichnis als lib, include oder inc zu bezeichnen.
2.2.3
Das Verzeichnis tests
In diesem Verzeichnis werden alle Unit-Tests gespeichert. Durch Unit-Tests wird gewährleistet, dass der Code auch funktionsfähig bleibt, wenn er immer weiter wächst und sich in der Lebenszeit der Applikation verändert. Wenn die Applikation entwickelt wird, muss vorhandener Code oft überarbeitet und geändert werden (das bezeichnet man als Refaktorierung), damit neue Funktionalitäten eingebaut werden können oder als Folge davon, dass anderer Code in die Applikation aufgenommen wird. Zwar wird innerhalb der PHP-Welt Testcode selten als sonderlich wichtig erachtet, doch Sie werden sehr, sehr dankbar sein, wenn Sie für Ihren Code Unit-Tests haben.
2.2.4
Das Verzeichnis public
Um die Sicherheit einer Webapplikation zu verbessern, sollte der Webserver nur auf jene Dateien direkten Zugriff haben, die er für seine Arbeit benötigt. Weil das Zend Framework mit dem Front-Controller-Pattern arbeitet, werden alle Webanfragen durch eine einzige Datei kanalisiert, meist hat sie den Namen index.php. Diese Datei ist die einzige PHPDatei, auf die der Webserver zugreifen muss. Also wird sie im Verzeichnis public gespeichert. Andere übliche Dateien, auf die direkt zugegriffen wird, sind Bild-, CSS-(Cascading Style Sheets) und JavaScript-Dateien. Also haben diese alle ihr eigenes Unterverzeichnis im Verzeichnis public.
35
2 Hello Zend Framework! Nachdem wir nun das von einer Zend Framework-Webapplikation verwendete Verzeichnissystem kennengelernt haben, machen wir weiter und fügen Dateien hinzu, um eine sehr einfache Applikation zu schaffen, die auf einer Seite ein wenig Text ausgibt.
2.3
Hello World: Datei für Datei Um eine einfache Hello World-Applikation zu erstellen, müssen wir innerhalb unserer Verzeichnisstruktur vier Dateien erstellen: eine Bootstrap-Datei, eine ApacheSteuerungsdatei (.htaccess), eine Controller-Datei und ein View-Template. Eine Kopie von Zend Framework selbst muss dem library-Verzeichnis ebenfalls hinzugefügt werden. Das finale Programm wird dann die in Abbildung 2.3 gezeigte Seite ausgeben. Das Resultat ist eine Webseite mit sehr wenig Text, und der dafür erforderliche Code, um diese scheinbar so einfache Aufgabe zu erfüllen, erscheint lang und einschüchternd. Wenn die Komplexität einer Applikation wächst, ist der zusätzliche Code, der für die neuen Funktionalitäten erforderlich ist, relativ klein. Dann werden die Vorteile des MVCSystems offensichtlich, die in einem solch kleinen Beispiel wie diesem hier nicht gleich erkennbar sind. Beginnen wir mit der Bootstrap-Datei, mit der die Applikation gestartet wird.
2.3.1
Bootstrapping
Als Bootstrapping bezeichnet man den Code, der die Applikation initialisiert und konfiguriert. Durch das Front-Controller-Pattern ist die Bootstrap-Datei die einzige Datei, die im Verzeichnis public stehen muss. Sie trägt meist den Namen index.php. Weil diese Datei für alle Seitenanfragen verwendet wird, wird sie auch zum Einrichten der Umgebung einer Applikation und des Controller-Systems des Zend Frameworks genommen. Anschließend startet sie die Applikation selbst (siehe Listing 2.1).
Abbildung 2.3 Die Hello World-Applikation produziert im Browser die Worte „Hello World!“. Eine minimale Zend Framework-Applikation benötigt .htaccess-, Bootstrap-, Controller- und View-Dateien, die in Zusammenarbeit dieses Werk produzieren.
36
2.3 Hello World: Datei für Datei Listing 2.1 Die Bootstrap-Datei index.php initialisiert und startet die Applikation.
Richtet die Umgebung ein
Setzt den Pfad Liest Zend_Controller_ Front-Instanz aus
// set up controller $frontController = Zend_Controller_Front::getInstance(); $frontController->setControllerDirectory('../application/controllers'); // run! $frontController->dispatch();
Schauen wir uns diese Datei etwas detaillierter an. Die meiste Arbeit, die in der BootstrapDatei anfällt, ist eine Initialisierung in der einen oder anderen Form. Anfangs wird die Umgebung korrekt eingerichtet n, um zu gewährleisten, dass alle Fehler oder Anmerkungen dargestellt werden. (Machen Sie das nicht auf Ihrem Produktionsserver!) PHP 5.1 hat neue Zeit- und Datumsfunktionalitäten eingeführt und muss darum wissen, wo in der Welt wir uns gerade befinden. Es gibt mehrere Wege, um dies einzurichten, doch die leichteste Methode ist der Aufruf von date_default_timezone_set(). Das Zend Framework geht davon aus, dass sich das library-Verzeichnis im Pfad befindet. Um das für eine globale Library einzurichten, kann man am schnellsten die include_path-Einstellung direkt in der php.ini vornehmen. Leichter übertragbar (vor allem, wenn Sie auf einem Server mehr als eine Version des Frameworks verwenden) ist die Methode, den include-Pfad innerhalb der Bootstrap-Datei zu setzen, wie wir es hier machen o.
php_include
Zend Framework-Applikationen sind nicht von einer bestimmten Datei abhängig, doch es ist ganz praktisch, wenn man schon früh ein paar Hilfsklassen geladen hat. Mit Zend_Loader::loadClass() wird die korrekte Datei für den gegebenen Klassennamen aufgenommen. Die Funktion konvertiert die Unterstriche im Namen der Klasse in Verzeichnisseparatoren und bindet dann nach einer Prüfung auf Fehler die Datei ein. Als Resultat haben die Codezeilen Zend_Loader::loadClass('Zend_Controller_Front');
und include_once 'Zend/Controller/Front.php';
das gleiche Endergebnis. Mit Zend_Debug::dump() werden Debugging-Informationen über eine Variable ausgegeben, indem ein formatierter var_dump()-Output geboten wird.
37
2 Hello Zend Framework! Der abschließende Abschnitt der Bootstrap-Datei richtet den Front-Controller ein und startet ihn. Die Front-Controller-Klasse Zend_Controller_Front implementiert das Singleton-Designpattern, und somit verwenden wir die statische Funktion getInstance(), um es auszulesen p. Ein Singleton-Design ist für einen Front-Controller angemessen, weil darüber gewährleistet ist, dass es nur eine Instanz des Objekts gibt, die die Anfrage verarbeitet. Der Front-Controller fängt standardmäßig alle geworfenen Exceptions ab und speichert sie im erstellten Antwortobjekt ab. Dieses Antwortobjekt enthält alle Informationen über die Antwort auf den angeforderten URL, und bei HTML-Applikationen gehören dazu die HTTP-Header, der Seiteninhalt und alle möglicherweise geworfenen Exceptions. Der Front-Controller sendet die Header automatisch und stellt den Seiteninhalt dar, wenn er die Verarbeitung der Anfrage abgeschlossen hat. Im Falle von Exceptions wird das Zend Framework-Plug-in ErrorHandler die Anfrage an eine Action-Methode namens error umleiten, die sich in einem Controller namens ErrorController befindet. So können wir sicher sein, dass wir die für den Anwender dargestellte Fehlermeldung kontrollieren und möglicherweise sogar weitere Hilfe anbieten können. Wir werden dieses Feature später implementieren. Um die Applikation zu starten, rufen wir die dispatch()-Methode des Front-Controllers auf. Diese Funktion wird automatisch ein Anfrage- und ein Antwortobjekt erstellen, um den Input und den Output der Applikation zu kapseln. Dann wird sie einen Router erstellen, der herausfinden soll, welchen Controller und welche Action der Anwender angefordert hat. Anschließend wird ein Dispatcher-Objekt erstellt, um die korrekte Controller-Klasse zu laden und die Elementfunktion (member function) der Action aufzurufen, die die „eigentliche“ Arbeit macht. Schließlich gibt der Front-Controller wie bereits erwähnt die Daten innerhalb des Antwortobjekts aus, und für den Anwender wird eine Webseite dargestellt.
2.3.2
Apache .htaccess
Damit alle Webanfragen, die sich nicht auf Bilder, Skripte oder Stylesheets beziehen, auch wirklich an die Bootstrap-Datei geleitet werden, wird das Apache-Modul mod_rewrite eingesetzt. Dieses kann direkt in der Apache-Datei httpd.conf oder in einer lokalen Apache-Konfigurationsdatei namens .htaccess, die sich im Verzeichnis public befindet, konfiguriert werden. Listing 2.2 zeigt die .htaccess-Datei, die für das Zend Framework erforderlich ist. Listing 2.2 Die Bootstrap-Datei: public/.htaccess
# Rewrite rules for Zend Framework RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteRule .* index.php
38
Fährt nur fort, wenn angeforderter URL keine Datei auf Festplatte ist
Leitet Anfrage auf index.php auf Festplatte um
2.3 Hello World: Datei für Datei Zum Glück ist dies nicht das komplizierteste Set von Apache-mod_rewrite-Regeln und können somit leicht erklärt werden. Die Anweisung RewriteCond und der Befehl RewriteRule darin instruieren Apache, alle Anfragen an index.php zu routen, falls die Anfrage nicht exakt auf eine Datei passt, die im Verzeichnisbaum public existiert. So können wir alle statischen Ressourcen bereitstellen, die im public-Verzeichnis platziert sind, wie z. B. JavaScript-, CSS- und Bilddateien, alle anderen Anfragen hingegen werden an unsere Bootstrap-Datei geleitet, wo der Front-Controller sich darum kümmern kann, was für den Anwender dargestellt werden soll.
2.3.3
Index-Controller
Das Front-Controller-Pattern ordnet den vom Anwender angeforderten URL einer bestimmten Elementfunktion (der Action) innerhalb einer speziellen Controller-Klasse zu. Diesen Vorgang nennt man Routing und Dispatching (etwa Leiten und Verteilen, Zuordnen). Bei den Controller-Klassen ist eine strikte Namenskonvention erforderlich, damit der Dispatcher die korrekte Funktion finden kann. Der Router erwartet, eine Funktion namens {actionName}Action()innerhalb der Klasse {ControllerName}Controller aufrufen zu können. Diese Klasse muss sich in einer Datei namens {ControllerName}Controller.php befinden. Wenn in der Anfrage entweder der Controller oder der Action-Name nicht enthalten ist, wird der Standard „index“ verwendet. Ein Aufruf von http://zfia.example.com/ wird dazu führen, dass die „index“-Action des Index-Controllers gestartet wird. Entsprechend wird ein Aufruf von http://zfia.example.com/test dazu führen, dass die „index“Action des Test-Controllers gestartet wird. Wie Sie später sehen werden, ist dieses Mapping sehr flexibel, aber der Standard deckt die meisten Szenarien ab. Innerhalb der Front-Controller-Implementierung des Zend Frameworks erwartet der Dispatcher eine Datei namens IndexController.php innerhalb des application/controllersVerzeichnisses. Diese Datei muss eine Klasse namens IndexController enthalten, und diese Klasse muss mindestens eine Funktion namens indexAction() aufweisen. Listing 2.3 zeigt die Datei IndexController.php, die für die Hello World-Applikation benötigt wird. Listing 2.3 Der Index-Controller: application/controllers/IndexController.php view->assign('title', 'Hello World!'); } }
Wie Sie sehen können, ist IndexController eine Kindklasse von Zend_Controller_ Action, das die Anfrage- und Antwortobjekte für den Zugriff auf die von der Applikation empfangenen Daten enthält und für die Daten, die neben einigen praktischen Hilfsfunktio-
39
2 Hello Zend Framework! nen zur Steuerung des Programmflusses wieder an den Anwender zurückgesendet werden. Für Hello World muss diese Funktion indexAction() der view-Eigenschaft eine Variable zuweisen, die wir von einer Action-Hilfsklasse namens Zend_Controller_Action_ ViewRenderer (auch als ViewRenderer bezeichnet) bekommen. Anmerkung Ein Action-Hilfsklasse ist eine Klasse, die sich mit dem Controller verbindet, um für Actions spezifische Dienste zu bieten. Sie erweitern die Funktionalität eines Controllers ohne die Verwendung der Vererbung und können somit in verschiedenen Controllern und Projekten wiederverwendet werden.
Die Action-Hilfsklasse ViewRenderer führt für uns zwei hilfreiche Funktionen aus: Erstens erstellt er ein Zend_View-Objekt, bevor unsere Action aufgerufen wird, und stellt es auf die $view-Eigenschaft der Action ein, was uns erlaubt, der View innerhalb der Action Daten zuzuweisen. Zweitens rendert er automatisch nach Beendigung unserer Action das korrekte View-Template im Antwortobjekt, nachdem die Controller-Action abgeschlossen ist. So bleibt gewährleistet, dass die Action-Funktionen unseres Controllers sich auf die eigentliche Arbeit konzentrieren können und sich nicht um die Verschaltung des Frameworks kümmern müssen. Doch welches ist denn das „korrekte View-Template“? Der ViewRenderer sucht im view/scripts-Verzeichnis nach einer Template-Datei, die nach der Action benannt ist und die Endung .phtml trägt, in einem Ordner, der nach dem Controller benannt ist. Das bedeutet für die Index-Action im Index-Controller, dass sie nach der View-Template-Datei view/scripts/index/index.phtml sucht. Wie bereits angemerkt, wird der Body der Antwort vom Front-Controller automatisch ausgegeben, und somit wird alles, was wir dem Body zuweisen, im Browser dargestellt. Dafür muss kein Echo eingesetzt werden. Zend_View ist die View-Komponente der MVC-Troika und ein recht einfaches, auf PHP basierendes Template-System. Wie wir gesehen haben, wird die assign()-Funktion verwendet, um Variablen vom Body des Hauptcodes an das Template zu übergeben, welches dann in der View-Template-Datei verwendet werden kann.
2.3.4
View-Template
Das View-Skript für unsere Applikation, index.phtml, wird im Unterverzeichnis views/ scripts/index gespeichert. Der ViewRenderer befolgt die praktische Konvention, alle View-Dateien mit der Endung .phtml zu versehen. Somit ist auf den ersten Blick erkennbar, dass sie nur zur Darstellung gedacht sind. Natürlich kann man das ganz einfach über Verändern der $_viewSuffix-Eigenschaft des ViewRenderers ändern. Auch wenn dies nur eine einfache Applikation ist, haben wir für alle View-Templates eines jeden Controllers ein separates Verzeichnis, weil die Applikation deutlich leichter zu verwalten ist, wenn sie später einmal wächst. Listing 2.4 zeigt das View-Template.
40
2.3 Hello World: Datei für Datei Listing 2.4 Das View-Template: views/scripts/index/index.phtml <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> escape($this->title);?> Konvertiert Sonderzeichen in HTML-Entity Repräsentationen
escape($this->title);?>
Weil Zend_View eine auf PHP basierende Template-Engine ist, verwenden wir PHP innerhalb der Datei, um die Daten von Model und Controller darzustellen. Die Template-Datei (in diesem Fall index.phtml) wird innerhalb einer Elementfunktion von Zend_View ausgeführt. Also steht $this innerhalb der Template-Datei zur Verfügung und bildet das Tor zur Funktionalität von Zend_View. Alle der View aus dem Controller heraus zugewiesenen Variablen sind direkt als Eigenschaften von $this verfügbar, was man durch Verwendung von $this->title in index.phtml sehen kann. Auch eine Reihe von Hilfsklassenfunktionen steht über Templates zur Verfügung, wodurch sie einfacher zu schreiben sind. Die am häufigsten verwendete Hilfsklassenfunktion ist escape(). Mit dieser Funktion wird sichergestellt, dass der Output HTML-sicher ist, und sie hilft Ihnen dabei, Ihre Site vor XSS-Angriffen (Cross-Site Scripting) zu sichern. Alle Variablen, von denen nicht erwartet werden kann, dass sie darstellbares HTML enthalten, sollten über die escape()Funktion ausgegeben werden. Zend_View ist so gestaltet, dass damit die Erstellung neuer Hilfsklassenfunktionen erleichtert wird. Für eine optimale Flexibilität besteht die Konvention darin, dass View-Helper-Funktionen ihre Daten zurückgeben, und die Template-Datei gibt sie dann für den Browser aus. Wenn diese vier Dateien an Ort und Stelle sind, haben wir eine minimale Zend Framework-Applikation erstellt, bei der alle Bestandteile für eine komplette Website schon vorhanden sind. Sie sollten nun ein grundlegendes Verständnis dafür haben, wie alle Teile ineinander greifen. Als Nächstes schauen wir uns an, was im Code des Zend Frameworks passiert, der die MVC-Grundlage bietet, auf dem unser Code aufbaut.
Konkrete Instanz(en) von Zend_Db_Table (Model) Konkrete Instanz(en) von Zend_Controller_Action
Erstellt Zend_Db_Rows und Zend_Db_Rowsets
Nutzt verschiedene Zend_ActionHilfsklassen
Zend_View (View)
Zend_Controller_Response_Http
Baut Darstellung auf
Nutzt verschiedene Zend_ActionHilfsklassen
Abbildung 2.4 Die Interaktion der verschiedenen Zend Framework-Klassen in einer MVCApplikation
Antwort an den Browser
2.4
Wie MVC im Zend Framework angewendet wird Zwar gibt es anscheinend viele unterschiedliche Wege, wie man Webanfragen an den Code innerhalb einer Webapplikation routen kann, doch sie können in zwei Gruppen sortiert werden: Page-Controller und Front-Controller. Ein Page-Controller arbeitet mit separaten Dateien für jede Seite (oder Gruppe von Seiten), aus denen die Website besteht, und das ist der traditionelle Weg zur Erstellung der meisten PHP-Websites. Das bedeutet, dass die Steuerung der Applikation dezentral auf viele unterschiedliche Dateien verlagert wird, was zu einem sich wiederholenden Code oder – schlimmer noch – einem sich wiederholenden und leicht veränderten Code führen kann, der zu solchen Problemen wie verlorene Sessions führen kann, wenn eine der Dateien keinen session_start() ausführt. Ein Front-Controller hingegen zentralisiert alle Webanfragen in einer einzigen Datei, die meist index.php genannt wird und sich im Root-Verzeichnis der Website befindet. Es gibt durch dieses System verschiedene Vorteile. Der offensichtlichste ist, dass es weniger duplizierten Code gibt, und dass es einfacher ist, die URLs, die eine Website besitzt, vom eigentlichen Code zu separieren, der für die Generierung der Seiten verwendet wird. Im Allgemeinen werden die Seiten über zwei zusätzliche Parameter dargestellt, die der Datei index.php übergeben wird, um solche URLs wie die folgende zur Darstellung einer list-Seite zu erstellen: index.php?controller=news&action=list
Wie wir in Kapitel 1 ausgeführt haben, verwendet das Zend Framework ein FrontController-Pattern, das mit dem Model-View-Controller-Pattern gekoppelt ist, um auf eine Anfrage des Browsers zu antworten. Jedes Pattern besteht aus mehreren Klassen (siehe Abbildung 2.4).
42
2.4 Wie MVC im Zend Framework angewendet wird Ein wichtiges Ziel der meisten modernen Webapplikationen ist, dass die URLs „gut“ aussehen sollen, damit sich die Anwender diese URLs besser merken können und damit es für Suchmaschinen wie Yahoo! oder Google einfacher wird, die Seiten der Website zu indexieren. Ein Beispiel so eines lesefreundlichen URLs wäre etwa http://www.example.com/ news/list, weil dieser URL keine Zeichen wie ? oder & enthält, und der Anwender daraus schließen kann, was dargestellt wird (eine Liste von Nachrichtenelementen). Der FrontController des Zend Frameworks verwendet eine Subkomponente, die Router genannt wird und standardmäßig anwenderfreundliche URLs unterstützt.
2.4.1
Der Controller im Zend Framework
Der Code für den Front-Controller im Zend Framework ist über eine Reihe verschiedener Klassen verteilt, die zusammenarbeiten, um eine sehr flexible Lösung für das Problem des Routings einer Webanfrage an die korrekte Stelle zu bieten. Zend_Controller_Front ist die Grundlage, und damit werden alle von der Applikation empfangenen Anfragen verarbeitet und die eigentliche Arbeit an Action-Controller delegiert. 2.4.1.1
Die Anfrage
Die Anfrage ist in einer Instanz von Zend_Controller_Request_Http gekapselt, die den Zugriff auf die gesamte Umgebung von HTTP-Anfragen bietet. Was ist eine Anfrageumgebung? Darin sind alle von der Applikation empfangenen Variablen enthalten, dazu relevante Controller-Parameter wie die Controller- und Action-Router-Variablen. Die HTTP-Anfrageumgebung enthält alle Superglobalen ($_GET, $_POST, $_COOKIE, $_SERVER und $_ENV) und dazu den Basispfad zur Applikation. Der Router platziert auch die Modul-, Controller- und Action-Namen in das Anfrageobjekt, wenn er sie herausgefunden hat. Zend_Controller_Request_Http stellt die Funktion getParam() bereit, damit die Applikation die Anfragevariablen sammeln kann und somit der Rest der Applikation vor einer Änderung in der Umgebung geschützt wird. Eine Anfrageumgebung mit Befehlszeile würde beispielsweise die HTTP-spezifischen Elemente nicht enthalten, jedoch die an das Skript übergebenen Befehlszeilenargumente umfassen. Somit wird der Code unverändert arbeiten, wenn er als Webanfrage oder als Befehlszeilenskript gestartet wird: $items = $request->getParam('items');
Generell sollte das Anfrageobjekt von der Applikation als read only behandelt werden, weil die vom Anwender gesetzten Werte nicht geändert werden sollten. Überdies enthält Zend_Controller_Request_Http auch Parameter, die in der Startup-Phase der Applikation gesetzt und dann je nach Bedarf von den Action-Funktionen ausgelesen werden können. Diese können verwendet werden, um bei Bedarf zusätzliche Informationen vom FrontController an die Action-Methoden übergeben werden. Nachdem der Front-Controller das Anfrageobjekt aus den Superglobalen ausgelesen und gesetzt hat, startet er dann den Router.
43
2 Hello Zend Framework! 2.4.1.2
Routing
Der Router bestimmt, welcher Code basierend auf den Parametern der Anfrage gestartet werden soll. Das wird von einer Klasse erledigt, die Zend_Controller_Router_Interface implementiert. Das Framework stellt auch Zend_Controller_Router_Rewrite bereit, das die meisten Routing-Anforderungen abwickelt. Ein Routing funktioniert so, dass der Teil des URI nach dem Basis-URL (als URIEndpunkt bezeichnet) genommen und dann in separate Parameter auseinander gepflückt wird. Für einen Standard-URL wie http://example.com/index.php?controller=news& action=list wird diese Aufteilung durchgeführt, indem einfach das Array $_GET gelesen und nach den Controller- und Action-Elementen gesucht wird. Es wird erwartet, dass die meisten Applikationen (wie andere moderne Applikationen), die mittels Zend Framework erstellt werden, mit schön geformten URLs der Form http://example.com/news/list arbeiten. In diesem Fall wird der Router die relevanten Variablen im superglobalen Array $_SERVER nutzen, um zu bestimmen, welcher Controller und welche Action angefordert wurden. Nachdem der Controller und die Action bestimmt wurden, wird neben etwaigen anderen Controller-Action-Methoden, die von der Applikation spezifiziert sein können, die korrekte Controller-Action-Methode gestartet. Das nennt man Dispatching, und es wird vom Front-Controller durchgeführt, nachdem der Router seine Arbeit abgeschlossen hat. 2.4.1.3
Dispatching
Mit Dispatching wird der Prozess bezeichnet, die korrekte Methode in der korrekten Klasse aufzurufen. Wie alle Komponenten im Zend Framework enthält der StandardDispatcher ausreichende Funktionalitäten für beinahe jede Situation. Wenn Sie aber etwas Spezielleres benötigen, können Sie sich ganz einfach einen eigenen Dispatcher schreiben und ihn im Front-Controller einbauen. Zentral vom Dispatcher gesteuert werden die Formatierung des Controller-Klassennamens, die Formatierung des Namens der ActionMethode und der Aufruf der eigentlichen Action-Methode. Im Zend_Controller_Dispatcher_Standard werden die Regeln hinsichtlich der Großund Kleinschreibung durchgesetzt, z. B. dass das Namensformat des Controller immer TitleCase ist und nur alphanumerische Zeichen enthält (und das Zeichen für den Unterstrich). Die dispatch()-Methode des Dispatchers ist für das Laden der ControllerKlassendatei verantwortlich, instanziiert die Klasse und ruft dann in dieser Klasse die Action-Methode auf. Wenn Sie beschlossen haben, dass die Struktur neu organisiert werden muss, damit jede Action in ihrer eigenen Klasse innerhalb eines Verzeichnisses steht, welches nach dem Controller benannt wurde, dann müssen Sie einen eigenen Dispatcher schreiben. An diesem Punkt übergibt der Dispatcher die Steuerung an die Action-Funktion der Controller-Klasse. Action-Controller sind separate Klassen, die im controllers-Unterverzeichnis des application-Verzeichnisses gespeichert sind.
44
2.4 Wie MVC im Zend Framework angewendet wird 2.4.1.4
Die Action
ist eine abstrakte Klasse, von der alle Action-Controller abgeleitet sind. Der Dispatcher erzwingt, dass Ihre Action-Controller von dieser Klasse abgeleitet sind, damit er auch ganz sicher erwarten kann, dass bestimmte Methoden verfügbar sind. Die Action enthält eine Instanz der Anfrage (um Parameter auslesen zu können) und eine Instanz der Antwort (um schreiben zu können). Der Rest der Klassen achtet darauf, dass das Schreiben von Actions und die Verwaltung von Änderungen von einer Action zur anderen einfach durchzuführen sind. Es gibt Accessor-Funktionen, um Parameter zu holen und zu setzen, und Umleitungsfunktionen, um ganz auf eine andere Action oder einen anderen URL umzuleiten.
Zend_Controller_Action
Vorausgesetzt, dass der Standard-Dispatcher verwendet wird, werden die ActionFunktionen alle nach dem Namen der Action gebildet und das Wort „Action“ angehängt. Sie können von daher erwarten, dass eine Controller-Action-Klasse Funktionen wie indexAction(), viewAction(), editAction(), deleteAction() usw. enthält. Jede dieser Actions ist eine diskrete Methode, die als Reaktion auf einen spezifischen URL gestartet wird. Verschiedene Aufgaben wollen Sie außerdem auch erledigt haben, egal welche Action gestartet wurde. Zend_Controller_Action bietet zwei Stufen der Funktionalität, um diese Anforderung zu erfüllen: init() und das Paar preDispatch() und postDispatch(). Die init()-Methode wird immer dann aufgerufen, wenn die Controller-Klasse konstruiert wird. Dadurch ähnelt sie sehr dem Standardkonstruktor, außer dass sie keine Parameter annimmt und es nicht nötig ist, dass die Elternmethode aufgerufen wird. und postDispatch() sind ein komplementäres Methodenpaar, das vor und nach jedem Aufruf einer Action-Methode gestartet wird. Bei einer Applikation, bei der nur eine Action als Antwort auf eine Anfrage gestartet wird, gibt es keinen Unterschied zwischen init() und preDispatch(), weil beide nur einmal aufgerufen werden. Wenn die erste Action-Methode die Funktion _forward() nutzt, um die Steuerung an eine andere Action-Methode zu übergeben, wird preDispatch() erneut gestartet, aber init() nicht. Um diesen Punkt zu veranschaulichen, können wir anhand von init() sicherstellen, dass nur Administratoren der Zugriff auf eine beliebige Action-Methode im Controller erlaubt wird, und wir können über preDispatch() die korrekte View-Skript-Datei setzen, die von der Action verwendet werden soll. preDispatch()
Nachdem die Action-Methode abgeschlossen ist, wird die Steuerung an den Dispatcher zurückgegeben, der dann je nach Erfordernis andere Actions starten wird. Wenn alle Actions abgeschlossen sind, wird der erstellte Output über das Antwortobjekt an den Anwender zurückgegeben. 2.4.1.5
Die Antwort
Das finale Glied in der Front-Controller-Kette ist die Antwort (Response). Für Webapplikationen gibt es Zend_Controller_Response_Http, doch wenn Sie eine Befehlszeilenapplikation schreiben, wäre Zend_Controller_Response_Cli passender. Das Antwortobjekt ist
45
2 Hello Zend Framework! sehr einfach und im Prinzip so ähnlich wie ein Behälter, in dem der gesamte Output aufbewahrt wird, bis die Verarbeitung durch den Controller abgeschlossen ist. Das kann sehr praktisch sein, wenn man mit Front-Controller-Plug-ins arbeitet, weil sie den Output der Action verändern können, bevor er an den Client zurückgeschickt wird. Zend_Controller_Response_Http enthält drei Arten von Informationen: Header, Body und Exception. Im Kontext der Antwort sind die Header HTTP- und keine HTML-Header. Jeder Header ist ein Array, das einen Namen und den dazugehörigen Wert enthält. Es ist möglich, dass sich im Container der Antwort zwei Header mit dem gleichen Namen, aber unterschiedlichen Werten befinden. Die Antwort enthält auch den HTTP-Antwort-Code (wie er in RFC 2616 definiert wird), der am Ende der Verarbeitung an den Client geschickt wird. Standardmäßig ist er auf 200 gesetzt, was für OK steht. Andere übliche AntwortCodes sind 404 (Not Found) und 302 (Found), der verwendet wird, wenn auf einen neuen URL umgeleitet wird. Wie wir später noch sehen werden, kann die Verwendung des Statuscodes 304 (Not Modified) sehr praktisch sein, wenn man auf Anfragen nach RSS-Feeds reagiert, weil damit beträchtliche Bandbreite gespart werden kann.
In den Body-Container der Antwort wird alles andere hineingesteckt, was an den Client zurückgeschickt werden muss. Bei einer Webapplikation ist damit alles gemeint, was Sie sehen, wenn Sie sich den Quellcode einer Webseite anschauen. Wenn Sie eine Datei an einen Client schicken, wird der Body die Inhalte der Datei enthalten. Um z. B. eine PDFDatei an den Client zu schicken, müsste man den folgenden Code nehmen: $filename = 'example.pdf'; $response = new Zend_Controller_Response_Http(); // HTTP-Header setzen $response->setHeader('Content-Type', 'application/pdf'); $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); $response->setHeader('Accept-Ranges', 'bytes'); $response->setHeader('Content-Length', filesize($filename)); // Datei laden, die in den Body gesendet werden soll $response->setBody(file_get_contents($filename)); echo $response;
Der finale Container innerhalb des Antwortobjekts beherbergt die Exceptions. Dies ist ein Array, das man durch Aufruf von $response->setException() ergänzen kann. Es wird vom Zend_Controller_Front verwendet, damit Fehler innerhalb des Codes nicht an den Client gesendet werden, was möglicherweise zur Veröffentlichung von privaten Informationen führt, die zur Kompromittierung Ihrer Applikation genutzt werden können. Natürlich wollen Sie während der Entwicklung die Fehler sehen. Also hat die Antwort eine Einstellung namens renderExceptions, die Sie auf true setzen können, damit der ExceptionText dargestellt wird. Um den Front-Controller zu erweitern, wurde ein Plug-in-System entwickelt. 2.4.1.6
Plug-ins für den Front-Controller
Die Architektur des Front-Controllers enthält ein Plug-in-System, damit Anwender-Code an bestimmten Stellen im Routing- und Dispatching-Prozess automatisch ausgeführt werden kann. Mit Plug-ins können Sie die Funktionalität des Routing- und Dispatching-
46
2.4 Wie MVC im Zend Framework angewendet wird Systems des Front-Controllers auf modulare Weise ändern. Diese Plug-ins sind so beschaffen, dass sie ganz leicht von einem Projekt zum nächsten transferiert werden können. Alle Plug-ins werden von Zend_Controller_Plugin_Abstract abgeleitet. Dabei gibt es sechs Event-Methoden, die überschrieben werden können: routeStartup() wird direkt vor Ausführung des Routers aufgerufen. routeShutdown() wird nach Beenden des Routers aufgerufen. dispatchLoopStartup() wird aufgerufen, kurz bevor der Dispatcher mit der Ausfüh
rung beginnt. preDispatch() wird immer aufgerufen, bevor eine Action ausgeführt wird. postDispatch() wird immer aufgerufen, nachdem eine Action ausgeführt wird. dispatchLoopShutdown() wird dann aufgerufen, nachdem alle Actions verteilt worden
sind. Wie Sie sehen können, gibt es drei Paare von Einstiegspunkten (sogenannte Hooks) im Prozess an drei verschiedenen Stellen, die eine immer feinstufigere Steuerung des Prozesses erlauben. Ein Problem mit dem aktuellen Router ist, dass eine Exception geworfen wird, wenn Sie einen nicht vorhandenen Controller angeben. Ein Front-Controller-Plug-in ist ein guter Weg, um eine Lösung in den Routing-Prozess zu injizieren und die Applikation auf eine nützlichere Seite umzuleiten. Zend Framework enthält zu diesem Zweck das Plug-in ErrorHandler, dessen Verwendung im Manual ausführlich erläutert wird. Nachdem wir uns nun den Controller-Teil von MVC eingehend angeschaut haben, wollen wir uns nun den View-Teil vornehmen, der von der Komponente Zend_View bereitgestellt wird.
2.4.2
Arbeit mit dem Zend_View
Die Klasse Zend_View trennt den View-Teil einer MVC-Applikation von der restlichen Anwendung. Es ist eine PHP-Template-Library, was bedeutet, dass der Code in den ViewSkripts in PHP geschrieben ist statt in einer anderen Pseudo-Sprache wie beispielsweise Smarty. Doch Zend_View kann ganz einfach so erweitert werden, dass es auch beliebige andere Template-Systeme unterstützt. Beginnen wir mit der Untersuchung von Zend_View, indem wir uns anschauen, wie Daten der View zugewiesen werden. Die Methode assign()von Zend_View wird für die Darstellung der Daten aus dem Model verwendet. Einfache Variablen können einer ViewVariable in folgender Weise zugewiesen werden: $view->assign('title', 'Hello World!');
Hiermit wird der String „Hello World!“ der Variable title zugewiesen. Alternativ können Sie über ein assoziatives Array mehrere Variablen simultan zuweisen:
Weil wir ja mit PHP5 arbeiten, können wir auch die magische Methode __set() nutzen, um zu schreiben: $view->title = 'Hello World!';
was der Variable title ebenfalls den String zuweisen wird. Egal welche dieser beiden Methoden Sie zum Zuweisen der Daten verwenden, die Daten aus dem Model oder dem Controller sind nun zur Verwendung im View-Skript bereit (das ist die Datei, die HTML und Code zum Output enthält). 2.4.2.1
Das View-Skript
Ein View-Skript ist so wie jede andere normale PHP-Datei, außer dass ihr Geltungsbereich in einer Instanz eines Zend_View-Objekts enthalten ist. Das bedeutet, dass es auf alle Methoden und Daten von Zend_View zugreifen kann, als wäre es eine Funktion innerhalb der Klasse. Die der View zugewiesenen Daten sind öffentliches Eigentum der View-Klasse und somit direkt zugreifbar. Überdies enthält die View Hilfsfunktionen, um das Schreiben von View-Skripts zu vereinfachen. Ein typisches View-Skript sieht etwa wie folgt aus:
Glossary
glossary) :?>
glossary as $item) : ?>
escape($item['term']);?>
escape($item['description']);?>
Wie Sie sehen können, handelt es sich hier um ein an HTML angelehntes PHP-Skript, weil die PHP-Befehle immer in eigenen -Tags stehen. Wir haben auch die alternative Konvention für Steuerungsschleifen verwendet, damit keine Klammern in separaten PHP-Tags vorkommen – es kann ganz schön kompliziert sein, Klammern einander zuzuordnen, wenn man mit vielen separaten PHP-Tags arbeitet. Beachten Sie, dass wir den Glossardaten misstrauen, die dem Skript zugewiesen wurden. Sie könnten ja von sonst woher stammen! Im Code, der zu diesem Buch gehört, werden die Daten über ein Array erstellt, doch sie könnten genauso gut von den Anwendern einer Website stammen. Um irgendwelche XSS-Schwachstellen auf unserer Website zu vermeiden, nutzen wir die Hilfsfunktion escape(), um zu gewährleisten, dass der Begriff und die Beschreibung kein eingebettetes HTML enthalten. Um zu verhindern, dass sich viel ähnlicher PHP-Code in mehreren View-Skripten wiederholt, nutzt man View-Hilfsfunktionen, um allgemein erforderliche Funktionalitäten bereitzustellen.
48
2.4 Wie MVC im Zend Framework angewendet wird 2.4.2.2
View-Hilfsfunktionen
enthält eine Reihe von hilfreichen Methoden, mit denen das Schreiben von View-Skripts einfacher wird. Diese Methoden bezeichnet man als View-Hilfsfunktionen, und sie sind in ihren eigenen Klassen im application/views/helpers-Unterverzeichnis abgelegt. Wie wir bereits gestehen haben, ist escape() die am häufigsten vorkommende ViewHilfsfunktion, die in die Klasse Zend_View selbst integriert ist. Jede andere Hilfsfunktion existiert in ihrer eigenen Klasse und wird automatisch von Zend_View geladen.
Zend_View
Wir wollen nun eine einfache Formatierungshilfsfunktion erstellen, um einen Bargeldbetrag darzustellen. Beachten Sie, dass wir einen Geldwert darstellen müssen, der auch negativ sein kann. In Großbritannien wäre für einen Wert von 10 die Darstellung £10.00, und für einen Wert von -10 würde -£10.00 dargestellt werden. Wir nutzen dann die Hilfsfunktion in unseren View-Skripten wie folgt:
He gave me formatCurrency(10);?>.
Das führt zu dem korrekt formatierten Betrag, wie er in Abbildung 2.5 gezeigt wird. Abbildung 2.5 Mit der View-Hilfsfunktion FormatCurrency wird das korrekte Währungssymbol an der richtigen Stelle dargestellt.
Alle Standard-View-Hilfsfunktionen verwenden das Klassenpräfix Zend_View_Helper und sind im Unterverzeichnis applications/views/helpers gespeichert. Sie können View-Hilfsfunktionen auch woanders speichern, aber dann müssten Sie Ihr eigenes Klassenpräfix verwenden. Unsere Formatierungshilfsklasse heißt Zend_View_Helper_FormatCurrency und wird in der Datei application/views/helpers/FormatCurrency.php (siehe Listing 2.5) gespeichert. Im Unterschied zur normalen Konvention im Framework ist dies einer der wenigen Fälle, wo der Klassenname nicht der gleiche ist wie der Dateipfad. Listing 2.5 Die View-Hilfsfunktion FormatCurrency class Zend_View_Helper_FormatCurrency { public function formatCurrency($value, $symbol='£') { $output = ''; Ignoriert $value, $value = trim($value); falls es keine Zahl ist if (is_numeric($value)) { if ($value >= 0) { $output = $symbol . number_format($value, 2); } else { $output = '-' . $symbol . number_format(abs($value), 2); } } return $output; }
}
49
2 Hello Zend Framework! Wie Sie sehen können, geben wir den Wert der Variable nicht als Teil des Outputs zurück n, solange wir nicht wissen, dass $value eine Zahl ist. Damit können wir etwas sicherer sein, nicht aus Versehen einen XSS-Angriffspunkt einzubauen. Der Name der Methode in der Hilfsklasse ist der gleiche wie die Methode, die im ViewSkript aufgerufen wird: in unserem Fall hier formatCurrency(). Intern ist bei Zend_View die magische Funktion __call() implementiert, um unsere Hilfsklasse zu finden und die Methode formatCurrency() auszuführen. Tipp Wenn Sie Ihre View-Hilfsfunktionen erstellen, müssen Sie bei den Namen auf Groß- und Kleinschreibung achten. Der Klassenname wird als in Kamelschrift (CamelCase) mit Unterstrichen geschrieben, aber der Methodenname als camelCase, d. h. der Klassenname muss mit einem Großbuchstaben anfangen und der Methodenname mit einem Kleinbuchstaben.
View-Hilfsfunktionen sind zentral wichtig, um häufig vorkommenden Code aus Ihren ViewSkripts zu extrahieren, und dabei zu gewährleisten, dass sie leicht zu pflegen sind. Also sollten die View-Hilfsfunktionen überall wo möglich verwendet werden, um die View-SkriptDateien zu vereinfachen. Weil View-Skriptdateien den Output einer Applikation enthalten, ist es ganz wichtig, immer auch die Sicherheitsproblematik zu beachten, wenn man Daten an den Browser sendet. 2.4.2.3
Überlegungen zur Sicherheit
Wenn man View-Code schreibt, ist XSS das wichtigste Sicherheitsproblem, das beachtet werden muss. Solche Schwachstellen entstehen, wenn von Ihrer Website unerwartet HTML, CSS oder JavaScript dargestellt wird. Das passiert im Allgemeinen, wenn eine Website von Anwendern eingegebene Daten darstellt, ohne vorher zu prüfen, ob sie auch sicher dargestellt werden können. Das könnte passieren, wenn der Text aus einem Kommentarformular HTML enthält und auf der Seite eines Gästebuchs so, wie er ist, ausgegeben wird. Ein ziemlich berühmter XSS-Exploit ist der MySpace-Wurm von Samy. Dieser Exploit verwendete in dem Profil, das auf einer Seite dargestellt wurde, die alle Freunde des Anwenders auflistet, ein ganz spezielles JavaScript. Das JavaScript wurde dann automatisch ausgeführt, sobald man diese Freundeseite des Opfers angeschaut hat, und wenn dieser Anwender bei MySpace eingeloggt war, wurde Samy auch sein „Freund“. Wenn jemand also Ihre MySpace-Seite anschaut, dann wird er auch zu einem MySpace-„Freund“ von Samy gemacht. Das führte zu einer exponentiellen Steigerung der Freunde für Samy: In den ersten 20 Stunden wurden über eine Million MySpace-Profile infiziert. Zum Glück war der Code nicht sonderlich bösartig und hat den Anwendern nicht gleich ihre Passwörter geklaut. XSS-Schwachstellen können am einfachsten dadurch vermieden werden, indem man die Zeichen kodiert, die in HTML eine spezielle Bedeutung haben. Das heißt, Sie sollten alle Instanzen von < zu <, & zu & und > zu > ändern, damit der Browser sie als Literale und nicht als HTML behandelt. Im Zend Framework können Sie dafür mit der Hilfsfunktion escape() arbeiten. Jedes Mal, wenn Sie eine PHP-Variable in einer Template-
50
2.4 Wie MVC im Zend Framework angewendet wird Datei darstellen, sollten Sie dafür escape() verwenden, außer wenn HTML darin vorkommen soll. Wenn die Datei HTML enthalten muss, sollten Sie eine Säuberungsfunktion schreiben, damit nur HTML-Code darin enthalten ist, dem Sie vertrauen. Wir haben nun unsere Ausführung der View abgeschlossen und gehen weiter zum Model. Da finden wir die Innereien der Applikation vor, u. a. die Interaktion mit Datenbanken und Dateien.
2.4.3
Das Model in MVC
Wir haben in diesem Kapitel einige Zeit dem Controller und der View gewidmet, weil sie das erforderliche Minimum für eine Hello World-Applikation darstellen. In einer echten Applikation wird hingegen das Model-Element des MVC-Patterns wichtiger, weil sich darin die Businesslogik der Applikation befindet. In den meisten Fällen wird das Model auf irgendeine Weise mit einer Datenbank verbunden, in der die Daten zu finden sind, die durch die Applikation bearbeitet und dargestellt werden sollen. 2.4.3.1
Datenbankabstraktion mit Zend_Db
Zend_Db ist die Datenbankabstraktions-Library des Zend Frameworks und enthält eine Suite mit Funktionen, die Ihren Code von der zugrunde liegenden Datenbank-Engine abschottet. Das ist besonders praktisch, wenn Sie wollen, dass Ihre Applikation nicht mehr mit z. B. SQLite arbeiten soll und Sie beispielsweise zu MySQL oder Oracle wechseln wollen. Zend_Db verwendet das Factory-Designpattern, um die korrekte, datenbankspezifische Klasse bereitzustellen, die auf den Parametern beruht, die in die statische Methode factory() übergeben wurden. Um beispielsweise ein Zend_Db-Objekt für MySQL zu erstellen, nehmen Sie etwa folgenden Code: $params = array ('host' => '127.0.0.1', 'username' => 'rob', 'password' => '******', 'dbname' => 'zfia'); $db = Zend_Db::factory('PDO_MYSQL', $params);
Die Abstraktion Zend_Db baut hauptsächlich auf der PDO-Extension von PHP auf, die eine große Bandbreite von Datenbanken unterstützt. DB2 und Oracle werden auch außerhalb von PDO unterstützt. Alles sind Erweiterungen von Zend_Db_Adapter_Abstract, und somit ist das Interface im Wesentlich das gleiche, egal welche Datenbank zugrunde liegt. Was bekommen Sie von Zend_Db, was Sie nicht bei PDO selbst kriegen? Nun, Sie erhalten viele Hilfsfunktionen, um die Datenbank zu manipulieren, und auch einen Profiler, mit dem Sie herausfinden können, warum Ihr Code so langsam ist. Alle Standardfunktionen zum Einfügen, Aktualisieren, Auslesen und Löschen von Zeilen sind vorhanden. Im Manual sind all diese Funktionen sehr gut beschrieben, also machen wir mit der Datenbanksicherheit weiter.
51
2 Hello Zend Framework! 2.4.3.2
Sicherheitsprobleme bei Datenbanken
Die bekanntesten Sicherheitsprobleme bei Datenbanken rühren aus der SQL Injection her. Diese Sicherheitslücken entstehen, wenn die Anwender Ihren Code dazu bringen können, eine Datenbankabfrage zu starten, die von Ihnen nicht erlaubt wurde. Schauen Sie sich diesen Code an: $result = $db->query("SELECT * FROM users WHERE name='" . $_POST['name'] . "'");
Mit diesem typischen Code könnte man einen Anwender autorisieren, nachdem er ein Login-Formular abgeschickt hat. Der Programmierer hat darauf geachtet, dass die korrekte Superglobale $_POST verwendet wird, aber nicht geprüft, was darin enthalten ist. Nehmen wir an, dass $_POST['name'] diesen String enthält: ' OR 1 OR name = '
Das würde zu der folgenden, absolut zulässigen SQL-Anweisung führen: SELECT * from users where name='' OR 1 OR name= ''
Wie Sie sehen können, wird das OR 1 in der SQL-Anweisung dazu führen, dass alle Anwender von der Datenbanktabelle zurückgegeben werden. Mit einer SQL InjectionSchwachstelle wie dieser ist es möglich, dass ein Angreifer Informationen über Usernamen und Passwörter auslesen oder mit böser Absicht Datenbankzeilen löschen kann, was dazu führt, dass Ihre Applikation ihre Arbeit einstellt. Natürlich ist ganz offensichtlich, dass solche Angriffe über SQL Injection vermeidbar sind, wenn man darauf achtet, dass die in die SQL-Anweisung übergebenen Daten mit EscapeSequenzen (unter der korrekten Funktionalität Ihrer Datenbank) versehen werden. Bei MySQL können Sie die Funktion mysql_real_escape_string() verwenden, und bei PostgreSQL wäre das pg_escape_string(). Weil wir mit Zend_Db arbeiten, können wir die Elementfunktion quote() nehmen, um dieses Problem anzugehen. Die Methode quote() wird die korrekte zugrunde liegende, datenbankspezifische Funktion aufrufen, und falls es keine gibt, wird sie den String mittels der korrekten Regeln der daran beteiligten Datenbank mit Escape-Zeichen versehen. Das geht ganz einfach: $value = $db->quote("It's a kind of magic");
Eine alternative Lösung wäre, mit parametrisierten Abfragen zu arbeiten, wobei die Variablen durch Platzhalter gekennzeichnet und die Werte durch die Datenbank-Engine ersetzt werden. Dafür gibt es in Zend_Db die Funktion quoteInto(): $sql = $db->quoteInto('SELECT * FROM table WHERE id = ?', 1); $result = $db->query($sql);
Parametrisierte Abfragen werden generell als Best Practice betrachtet, weil sie zu schnelleren Datenbankzugriffen führen, vor allem zusammen mit Prepared Statements. In der Komponente Zend_Db bietet Zend Framework einen Datenbankzugriff auf höherer Ebene durch Nutzung der Komponente Zend_Db_Table. Diese bietet ein objektorientiertes
52
2.4 Wie MVC im Zend Framework angewendet wird Interface für eine Datenbanktabelle und deren damit verknüpfte Zeilen und vermeidet somit die Notwendigkeit, in jedem Model häufig vorkommende SQL-Anweisungen zu schreiben. 2.4.3.3
Eine Interaktion auf höherer Ebene mit Zend_Db_Table
Beim Programmieren des Models einer MVC-Applikation wollen wir nicht unbedingt auf der Ebene der Datenbankabfragen arbeiten, wenn es sich vermeiden lässt, weil wir uns über die Business-Logik der Applikation Gedanken machen wollen und nicht gerade über die Feinheiten, wie man mit einer Datenbank interagiert. Das Framework enthält Zend_Db_Table, das ist eine Implementierung des Table-Data-Gateway-Patterns. Diese bietet eine Abstraktion auf höherer Ebene, um mit Daten aus der Datenbank zu arbeiten. Zend_Db_Table arbeitet hinter den Kulissen mit Zend_Db und stellt eine statische Klassenfunktion namens setDefaultAdapter() bereit, um den Datenbankadapter so einzustellen, dass er mit allen Instanzen von Zend_Db_Table verwendet werden kann. Das wird in der Bootstrap-Datei gewöhnlich wie folgt eingerichtet: $db = Zend_Db::factory('PDO_MYSQL', $params); Zend_Db_Table::setDefaultAdapter($db);
Wir arbeiten nicht direkt mit Zend_Db_Table. Stattdessen erstellen wir eine Kindklasse, die die Datenbanktabelle repräsentiert, mit der wir arbeiten wollen. Zum Zwecke dieser Erläuterungen werden wir davon ausgehen, dass wir eine Datenbanktabelle namens news haben, in der sich die Spalten id, date_created, created_by, title und body befinden, mit denen wir arbeiten werden. Wir erstellen nun eine Klasse namens News: Class News extends Zend_Db_Table { protected $_name = 'news'; }
Über die Eigenschaft $_name wird der Name der Tabelle festgelegt. Wenn hier nichts angegeben wird, nimmt Zend_Db_Table den Namen der Klasse und achtet dabei auf Großund Kleinschreibung. Zend_Db_Table erwartet außerdem einen Primärschlüssel namens id (der bei einem Insert möglichst automatisch erhöht wird). Diese beiden Standarderwartungen können verändert werden, indem die geschützten Membervariablen $_name bzw. $_primary initialisiert werden. Hier folgt ein Beispiel: class LatestNews extends Zend_Db_Table { protected $_name = 'news'; protected $_primary = 'article_id'; }
Die Klasse LatestNews verwendet eine Datenbanktabelle namens news, die einen Primärschlüssel namens article_id enthält. Wenn Zend_Db_Table das Designpattern TableData-Gateway implementiert, stellt es eine Reihe von Funktionen bereit, um Daten zu sammeln, darunter find(), fetchRow() und fetchAll(). Die Funktion find() sucht die Zeilen über den Primärschlüssel, und die fetch-Methoden suchen Zeilen anhand anderer
53
2 Hello Zend Framework! Kriterien. Der einzige Unterschied zwischen fetchRow() und fetchAll() besteht darin, dass fetchRow()ein einzelnes Zeilenobjekt zurückgibt, während fetchAll() ein als Rowset (Zeilensatz) bezeichnetes Objekt zurückgibt, das einen ganzen Satz Zeilen enthält. Zend_Db_Table enthält auch Hilfsfunktionen namens insert(), update() und delete()zum Einfügen, Aktualisieren und Löschen von Zeilen. Zwar ist Zend_Db_Table schon an sich recht interessant, aber wie praktisch die Klasse ist, wird dann deutlich, wenn wir die Business-Logik hinzufügen. An diesem Punkt betreten wir den Bereich des Models im MVC. Sie können eine ganze Menge Sachen machen, und wir fangen damit an, für unser News-Model insert() und update() zu überschreiben. Nehmen wir zuerst einmal an, dass unsere news-Datenbanktabelle die folgende Definition hat (in MySQL): CREATE TABLE `news` ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY , `date_created` DATETIME NOT NULL , `date_updated` DATETIME NULL , `title` VARCHAR(100) NULL , `body` MEDIUMTEXT NOT NULL )
Die erste Business-Logik, die wir in der Klasse News (unserem Model) implementiert werden, wird beim Einfügen und Aktualisieren der Einträge automatisch die Felder date_ created und date_updated verwalten (siehe Listing 2.6). Diese Details laufen quasi „unter der Motorhaube“ ab, und der Rest des Systems muss sich darum nicht kümmern, darum ist es so ideal, sie im Model zu Platzieren. Listing 2.6 Automatische Wartung der Datenfelder in einem Model class News extends Zend_Db_Table { protected $_name = 'news'; public function insert($data) { if (empty($data['date_created'])) { $data['date_created'] = date('Y-m-d H:i:s'); } return parent::insert($data); Ruft insert()-Funktion } von Zend_Db_Table auf
public function update($data) { if (empty($data['date_updated'])) { $data['date_updated'] = date('Y-m-d H:i:s'); } return parent::update($data); }
Setzt Datumsfeld, falls noch nicht gesetzt
}
Dieser Code ist selbsterklärend. Wenn der Aufrufer beim Einfügen date_created nicht angegeben hat, setzen wir das heutige Datum ein und rufen die insert()-Funktion von Zend_Db_Table auf n. Das Aktualisieren läuft entsprechend, außer dass wir stattdessen das Feld date_updated ändern.
54
2.5 Zusammenfassung Wir können auch unsere eigenen Funktionen schreiben, um die von der Applikation geforderten Business-Logik entsprechende Daten auszulesen. Nehmen wir an, dass wir bei unserer Website die fünf letzten kürzlich erstellten News-Elemente auf der Homepage (der letzten drei Monate) darstellen wollen. Das kann über $news->fetchAll() im HomepageController erledigt werden. Doch es ist besser, die Logik in das News-Model zu verschieben, um die korrekte Schichtung der Applikation zu bewahren, damit sie bei Bedarf von anderen Controllern wieder verwendet werden kann: public function fetchLatest($count = 5) { $cutOff = date('Y-m-', strtotime('-3 months')) $where = array('date_created > ?' => $cutOff); $order = "date_created DESC"; return $this->fetchAll($where, $order, $count); }
Auch dies ist wiederum eine sehr einfache Funktionalität, die viel leistungsfähiger wird, wenn sie in der richtigen Schicht des MVC-Triumvirats platziert wird. Beachten Sie, dass wir ein parametrisiertes Array für die Variable $where verwenden, wodurch gewährleistet ist, dass Zend_Db uns gegen Angriffe mit SQL Injection schützt.
2.5
Zusammenfassung Wir haben mit dem Zend Framework eine einfache Hello World-Applikation geschrieben und die Art untersucht, wie das Designpattern Model-View-Controller auf unsere Applikationen angewendet wird. Sie sollten nun schon eine gute Vorstellung davon haben, warum unsere Applikationen durch das Zend Framework so einfach zu warten und zu schreiben sind. Ein Ideal, nach dem die Entwickler des Frameworks sich richten, wird die 80/20-Regel genannt. Jede Komponente ist dazu gedacht, 80 Prozent des Problemraums zu lösen, an den sie sich richtet, und enthält Anschlusspunkte, an die jene Entwickler andocken können, die die anderen 20 Prozent benötigen. Das Front-Controller-System bietet beispielsweise einen Router, der beinahe alle Anforderungen abdeckt. Wenn Sie einen spezialisierteren Router brauchen, können Sie Ihren eigenen ganz einfach in den Rest des Front-ControllerSetups einfügen. Entsprechend erlaubt es Zend_View_Abstract, andere Template-Engines hinzuzufügen, z. B. Smarty oder PHPTAL, wenn das enthaltene Zend_View für Ihre Applikation nicht passend ist. Wir machen nun weiter mit dem Erstellen einer vollständig funktionsfähigen CommunityWebsite, bei der die meisten im Framework enthaltenen Komponenten verwendet werden.
55
II Teil II – Eine Basisapplikation Nachdem wir nun wissen, was das Zend Framework ist, und die Grundlagen der Erstellung einer Applikation mit dem Framework verstanden haben, werden wir in den Kapiteln 3 bis 11 eine einsatzfähige Applikation erstellen, mit der wir im weiteren Verlauf des Buches arbeiten. Indem wir uns die für die Erstellung einer typischen Website zentralen Komponenten des Zend Frameworks anschauen, lernen wir, wie sie zu einem einheitlichen Ganzen zusammengeführt werden. Neben Authentifizierung, Zugriffskontrolle, Formularen, Such- und E-Mail-Funktionen wird es auch um die Implementierung von Ajax in einer MVC-Applikation gehen. Schließlich werden wir uns mit Problemen der Verwaltung und des Deployments beschäftigen, z.B. Versionskontrollen und Tests.
3 3
Websites mit dem Zend Framework erstellen
Die Themen dieses Kapitels
Entwickeln einer großen Zend Framework-Applikation Eine einfach zu wartende Bootstrap-Klasse erstellen Schreiben und Testen von Datenbank-Model-Klassen Um in diesem Kapitel die Features des Frameworks vorzustellen, werden wir eine Community-Website aufbauen, auf der Eltern sich über kindgerechte Ausflugsziele informieren können. Wir werden diese Website Places to take the kids! nennen. Der Aufbau einer solchen Community-Website erfordert eine Menge Zeit und Aufwand, und man könnte versucht sein, den einfachen Weg zu nehmen und PHP mit HTML zu vermischen. Unsere Website soll schon einige Jahre lang als wesentliche Infoquelle dienen können, und wenn man jetzt den guten Prinzipien des Software-Engineerings folgt, wird sich dieser Aufwand während der Lebensdauer des Projekts um ein Vielfaches auszahlen. Das MVC-System des Zend Frameworks wird uns dabei helfen, die Dinge gleich richtig anzupacken. Wir können sogar mit dem Nebeneffekt rechnen, schneller zu sein, weil wir die Prinzipien der Einfachheit und der „Konvention vor Konfiguration“ (convention over configuration) nutzen können, um sicher zu sein, dass der Code einfach zu schreiben und zu refaktorieren ist. Bevor wir uns an den Grundaufbau der Website machen, konzentrieren wir uns zuerst darauf, was wir eigentlich erstellen wollen und welche Features dafür gebraucht werden. Dann können wir eine Datenbank einrichten und die ersten Seiten programmieren.
59
3 Websites mit dem Zend Framework erstellen
3.1
Erste Planungsarbeiten für eine Website Wir können eine Website nicht ohne irgendeine Art von Spezifikation erstellen, doch eine umfassende Spezifikation würde zu lange dauern. Stattdessen wollen wir die Ziele unserer Site in einer einfachen Geschichte beschreiben. Nachdem wir uns angeschaut haben, was die Site leisten soll, nehmen wir uns die Probleme und Aufgabenstellungen innerhalb der Benutzerschnittstelle vor und machen uns dann an den Code.
3.1.1
Die Ziel der Site
Bei jeder Website gibt es nur eine einzige Frage, die aus Sicht des Benutzers beantwortet werden muss: Was macht diese Site für mich? Wenn wir herausfinden, wer auf diese Frage positive Antworten geben wird, haben wir die potenzielle Zielgruppe. Es gibt auch zweitrangige Fragen, die man stellen kann, z. B. wie die Site finanziert wird, aber diese sind in Relation weniger wichtig verglichen damit, dass wir sicherstellen wollen, dass die Site auch besucht wird. Man kann die Features einer Website zum Beispiel über kurze Absätze gewissermaßen in Draufsicht beschreiben und erläutern, wie ein bestimmtes Feature funktioniert. Der Hauptvorteil dieser „Stories“ im Vergleich zu einer ordentlichen Spezifikation besteht darin, dass sie in einer einfach zu verstehenden Sprache und ohne Fachjargon geschrieben sind. Wir können diesen gleichen Mechanismus auch dafür nehmen, die gesamte Website zu beschreiben: Places to take the kids! ist eine Website, durch die Eltern ihren Familienausflug genießen können, weil sie sicher sein können, dass die darin vorgestellten Ausflugsziele auch familiengerecht sind. Die Site soll eine Community von Nutzern anziehen, die Zielorte kommentieren und für andere empfehlen. Auf der Site gibt es einfache Mechanismen, um Orte als Ziele für einen Ausflug zu finden, indem man Kategorien durchsucht oder die Suchfunktionen nutzt. Die Anwender können verschiedene Orte als Zwischenstopps für eine geplante Reise speichern und sich später dann die Details ihrer Reiseplanung ausdrucken. Unsere Story für die Site vermittelt alles, was wir wissen müssen, um eine gute Website zu schaffen. Nun können wir uns an die Planung machen. Natürlich könnten wir auch noch viel mehr darüber schreiben, was die Site machen soll, aber das können wir auch den zukünftigen Entwicklungsphasen überlassen. Wir werden uns zuerst mit der Hauptfunktionalität der Website beschäftigen und damit, wie wir über Tests den Code verbessern. 3.1.1.1
Die wichtigsten Funktionen
Sammeln wir in einem Brainstorming eine Liste der Dinge, die die Website braucht, und erstellen daraus eine Mindmap. Unsere ersten Überlegungen finden sich in Abbildung 3.1. Was bei Mindmaps auch sehr schön ist: Man kann sie ganz leicht ergänzen. In diesem Fall ist der Abschnitt mit den Zielorten für die Site ganz wesentlich und muss deswegen am
60
3.1 Erste Planungsarbeiten für eine Website gründlichsten überdacht werden. Wir werden uns nicht sonderlich viel Gedanken über Preisausschreiben machen, weil die ganz nett, aber keine zentrale Anforderung sind, um die Ziele der Site zu erreichen.
Suche
Preisausschreiben
Landkreise
Bannerwerbung
Kategorisierung Beliebtheit
Werbung
Google AdWords
Weitere Zielorte Sponsoren Routenplaner mit Google Maps Beurteilung der Rezensionen durch Anwender
Zielorte
Places to take the kids
Rezensionen der Anwender
PDF-Ausdruck
Moderation der Rezensionen Administration
Zielorte hinzufügen/bearbeiten/löschen
Reiseplanung Benutzerverwaltung
E-Mail: An Freund senden Boards E-Mail: Passwort vergessen Benutzer-Login
Foren
Messages
unpassender Einträge melden
Profil Diskussionen
Abbildung 3.1 Mit Mindmaps kann man auf ideale Weise per Brainstorming die gewünschten Features für eine neue Website zusammentragen. Für diese Site fanden wir heraus, dass sieben Hauptbereiche der Website erforderlich sind, um die Hauptziele zu erreichen. Der Abschnitt Zielorte (Locations) ist hier am detailliertesten ausgeführt worden.
Wir haben unsere ersten Planungen für die Website abgeschlossen und widmen uns nun dem Erstellungsprozess. Wir haben uns vorgenommen, die Website zu erstellen, indem wir ein Feature zurzeit implementieren. Wenn die Site wächst, verändert sich der Ursprungscode, um sich den neuen Features anzupassen, und wir müssen darauf achten, dass wir die vorhandene Funktionalität nicht kaputt machen. Dabei werden uns die Unit-Tests helfen. 3.1.1.2
Die Arbeit mit Unit-Tests
Im Laufe unserer Arbeit werden wir für alles Tests schreiben, bei dem wir uns nicht sicher sind. So werden wir mit unserem Code immer vertrauter, können auf ihn bauen und ihn auch refaktorieren, wenn wir das Design verbessern. Es ist nicht sonderlich schwer, das Design zu verbessern, weil wir ja daran bisher kaum etwas gemacht haben! Auch wenn wir vorab schon eine ganze Menge Designvorarbeiten geleistet hätten, können wir sicher sein, dass alles, was wir beim Erstellen lernen, von großem Wert sein wird. Darum soll das auch jeweils mit eingebaut werden. Wir werden hier zwar nicht jeden einzelnen Test auf diesen Seiten erläutern, aber der begleitende Quellcode enthält alle Tests, die für uns erforderlich ist, damit wir uns auf den Code verlassen können. Durch die Tests erfahren wir, wie belastbar und vertrauenswürdig unser Code ist, wenn wir im weiteren Verlauf neue Features einbauen. Die Tests für Places werden hauptsächlich durch automatisierte Unit-Tests vorgenommen, weil Tests, bei denen jemand eine schriftliche Prozedur befolgen muss, nicht oft genug durchgeführt werden, um die aus dem überarbeiteten Code entstehenden Fehler abzufangen. Das MVC-System des Zend Frameworks enthält ein Antwort-Objekt. So können wir anhand von Unit-Tests prüfen, dass der HTMLOutput die korrekten Daten enthält und Elemente der Seitendarstellung ebenfalls testen.
61
3 Websites mit dem Zend Framework erstellen
3.1.2
Das Design der Benutzerschnittstelle
Wir müssen uns Gedanken darüber machen, wie unsere neue Website hinsichtlich der Benutzerschnittstelle (User Interface, UI) funktionieren soll. Ich bin Software-Ingenieur, kein Kreativdesigner, also wird es sicher das Beste sein, wenn ich mir nicht allzu viel Gedanken um das Design mache. Bei einer guten Benutzerschnittstelle geht es um viel mehr als das äußere Erscheinungsbild: Man muss auch bedenken, wie sie für den Anwender bedienbar ist. Wir müssen darauf achten, dass unsere Anwender leicht zu den Informationen finden, nach denen sie suchen. Die zentralen Features der UI, die wir auf unserer Places-Website berücksichtigen wollen, sind die Navigation über ein Menü und ein Suchsystem. Bei der Navigation wird es bei den Hauptpunkten verschiedene Unterpunkte geben, wobei das Hauptmenü stets sichtbar sein soll und der aktuelle Menülevel ebenfalls gezeigt wird. So wird eine gute Flexibilität möglich, aber das Menüs nicht überladen. Wir könnten auch eine Breadcrumb-Anzeige darstellen, damit die Anwender wissen, wo sie sich gerade auf der Site befinden und wie sie von der Homepage hierhin gekommen sind. Beim Design der Benutzerschnittstelle einer Site müssen wir uns Gedanken über die Features der Site machen, z. B.
Menüs und Navigation Seitenlayout Accessibility (Anwenderfreundlichkeit, Barrierefreiheit) Bilder Wir schauen uns nun die zentralen Probleme an, die hinsichtlich dieser Elemente berücksichtigt werden müssen. Diese bilden dann das Design-Briefing zur Erstellung des eigentlichen Look & Feels der Website. 3.1.2.1
Menüs und Navigation
Ein zentrales Feature, das wir uns für die Places-Website anschauen wollen, ist die Navigation – sowohl über ein Menü als auch über ein Suchsystem. Die Navigation wird ausklappbar mit Unterpunkten gestaltet, wobei das Hauptmenü sich stets sichtbar oben auf der Seite befinden soll und dann jeweils das aktuelle vertikale Untermenü gezeigt wird. Das ist ein sehr flexibler Ansatz, der das Menü auch nicht überlädt. Wir können auch eine Breadcrumb-Anzeige einbauen, damit der Anwender eine Vorstellung davon hat, wo er sich auf der Site befindet und wie er jeweils zur aktuellen Seite gekommen ist. So haben wir eine einfach zu navigierende Site, die gleichzeitig viel Erweiterungsspielraum vorhält, um neue Features einzubauen.
62
3.1 Erste Planungsarbeiten für eine Website 3.1.2.2
Das Seitenlayout
Weil es sich hier um eine Community-Site handelt, werden wir darauf achten, dass es viel Platz für Inhalte (Content) gibt und dass auch Anzeigen dezent platziert werden können, über die die Kosten für die Bereitstellung der Site bezahlt werden sollen. Das grundlegende Erscheinungsbild der Site soll langlebig sein, weil wir eine Marke aufbauen wollen und Communities im Allgemeinen Veränderungen gegenüber eher abgeneigt sind. Das bedeutet, dass wir ein Design brauchen, das mitwachsen kann, wenn wir die Site mit neuen Features verbessern. 3.1.2.3
Die Anwenderfreundlichkeit
Eine moderne Site muss für alle zugänglich sein. Das bedeutet, dass sie standardkonform sein muss, damit sie in allen modernen Browsern funktioniert, und dass wir auch Anwender mit Einschränkungen im Sehvermögen oder einer eingeschränkten koordinierten Bedienung mit der Maus berücksichtigen müssen. Wir müssen den Standard der Web Accessibility Initiative (WAI) stets im Hinterkopf behalten, wenn wir das Frontend der Website erstellen. 3.1.2.4
Bilder
Man sagt ja gerne, dass ein Bild mehr sagt als Tausend Worte, aber es kostet verglichen mit Worten auch deutlich mehr Zeit und Bandbreite! Wir werden für unsere Anwender anhand von Bildern das Aussehen der Site verbessern und den Wert bei der Nutzung der Site erhöhen. Wir werden natürlich bei jeder Rezension Bilder vom jeweils besprochenen Zielort vorhalten. Auch bei den Listen für die verschiedenen Angebote der Ausflugsziele kann man sehr gut mit Bildern arbeiten und die verschiedenen Abschnitte der Seiten kennzeichnen. Wenn man das alles in einem Design zusammenführt, hat man eine Site, die in etwa so wie in Abbildung 3.2 aussieht. In diesem Design gibt es vier Abschnitte: die Kopfzeile (der sogenannte Header) im ganzen oberen Bereich, die Hauptinhalte auf der linken Seite, eine Bannerwerbung rechts und eine Fußzeile, den Footer. Wir können uns nun den Code anschauen, der für die Erstellung der Site erforderlich ist, und starten mit dem ersten Einrichten der Verzeichnisstruktur, Bootstrap und dem Laden der Konfigurationsinformation. Wir schauen uns ebenfalls an, wie die View-Skripte erstellt werden, damit wir den Code für Kopf- und Fußzeile nicht in jedem Action-View-Skript wiederholen müssen.
63
3 Websites mit dem Zend Framework erstellen
Abbildung 3.2 Die Homepage für Places to take the kids! maximiert den für die Inhalte bereitstehenden Platz und ist gleichzeitig einfach zu nutzen.
3.1.3
Den Code planen
Wir haben die Ziele der Site zusammengestellt und uns angeschaut, wie die Benutzerschnittstelle funktionieren soll, und nun geht’s an die Strukturierung des PHP-Codes. Wie bei der Benutzerschnittstelle müssen wir darauf achten, dass der Code nicht eingeengt wird, wenn die Funktionalität und die Features der Site zunehmen. Wir wollen außerdem ein System, das uns von der internen Verschaltung soviel wie möglich abnimmt, damit wir uns beispielsweise nicht darum kümmern müssen, die korrekte Klasse in der Dateistruktur zu finden oder welcher Name für ein View-Skript zu wählen ist oder wie man sich auf Datenbanktabellen beziehen muss. Wir werden auch mit Ajax arbeiten und somit das Ausgabeformat des Views in HTML gelegentlich einmal ändern müssen. Das Zend Framework stellt für diese Anforderungen eine gute Wahl dar. Wie bereits in Kapitel 1 angesprochen, haben wir es beim Zend Framework mit einer flexiblen, robusten und gut unterstützten Plattform zu tun, die uns für die ganze Lebensdauer der Site begleiten kann. Durch das MVC-System des Zend Frameworks können wir unsere Site in separate Controller organisieren, die jeweils ihr eigenes Set an View-Skripts bekommen. Wir werden auf die Datenbanktabellen über Models zugreifen, mit denen wir Code in der Sprache des Problems anstatt in der Sprache der Datenbank schreiben können. Ein weiteres Feature des MVC-Systems sind die Module, mit denen wir eine Gruppe von zusammenhängenden Controllern, Views und Models gruppieren können. Wir halten mit dieser Funktionalität die unterschiedlichen logischen Anliegen der Website Places voneinander getrennt. Nach diesen ersten Planungen können wir nun mit dem Schreiben des Codes und der ersten Tests anfangen, müssen aber erst noch wissen, wo alles abgelegt werden soll.
64
3.2 Die ersten Programmzeilen
3.2
Die ersten Programmzeilen Wir können die ersten Programmierarbeiten vornehmen, indem wir eine Skelettstruktur schaffen, um darauf die erforderlichen Features aufzubauen. Das bedeutet, wir werden die nötigen Verzeichnisse einrichten, die Bootstrap-Datei schreiben, die Konfigurationsprobleme untersuchen und die Datenbank erstellen.
3.2.1
Die Verzeichnisstruktur
In Kapitel 2 haben wir uns die zentralen Verzeichnisse angeschaut, die wir für eine Zend Framework-Applikation brauchen – das nehmen wir also als Ausgangspunkt. Die PlacesWebsite ist etwas größer als Hello World, und somit brauchen wir noch weitere Verzeichnisse, um die Dateien verwaltbar zu halten (siehe Abbildung 3.3.).
Abbildung 3.3 Verzeichnisstruktur der Places-Website mit den zentralen Dateien für die Homepage
Wie bei Hello World organisieren wir die Verzeichnisstruktur mit dem Ziel, alle Dateien gut wiederfinden zu können. Das bedeutet, dass wir die gesamte Funktionalität sowohl logisch durch die Verwendung von Modulen als auch nach Aufgabenbereichen in separate Verzeichnisse für Models, Views und Controller aufteilen. Aus Gründen der Sicherheit wird das öffentliche Verzeichnis das einzige sein, aus dem der Webserver direkt Dateien bereitstellen kann. Also steckt darin nur eine Datei: die Datei index.php, die die BootstrapKlasse lädt, mit der es im nächsten Abschnitt weitergeht.
65
3 Websites mit dem Zend Framework erstellen
3.2.2
Die Bootstrap-Klasse
Die Bootstrap-Datei für die Applikation Hello World war sehr schlicht, und somit wurde sie in index.php gespeichert. Wenn eine Applikation wächst, ist größere Flexibilität erforderlich. Wir werden daher eine Bootstrap-Klasse in application/bootstrap.php erstellen, auf die dann von public/index.php referenziert wird. Wir fangen mit dem Bootstrap-Code aus Hello World an und verbessern ihn dann, damit er Konfigurationsdaten aufnehmen, die Datenbank initialisieren und automatisch die benötigten Klassen laden kann. Unsere erste Bootstrap-Klasse ist in Listing 3.1 gezeigt. Deren Details werden in den folgenden Abschnitten erläutert. Listing 3.1 Die grundlegende Bootstrap-Klasse: application/bootstrap.php
Richtet include-Pfad ein
// configure database and store to the registry $db = Zend_Db::factory($config->db); Zend_Db_Table_Abstract::setDefaultAdapter($db); Zend_Registry::set('db', $db); }
Konfiguriert
und speichert Datenbank
public function configureFrontController() { $frontController = Zend_Controller_Front::getInstance(); $frontController->setControllerDirectory(ROOT_DIR . '/application/controllers'); } public function runApp() {
66
Richtet FrontController ein
3.2 Die ersten Programmzeilen { $this->configureFrontController(); // run! $frontController = Zend_Controller_Front::getInstance(); $frontController->dispatch();
Startet Applikation
}
Die Bootstrap-Klasse hat drei Methoden: den Konstruktor (der die Umgebung initialisiert), configureFrontController() (der den Front-Controller einrichtet) und runApp() (startet die MVC-Applikation selbst). Somit können wir diesen Code wiederverwenden, wenn wir unsere Applikation testen. Der Konstruktor bereitet die Umgebung für unsere Applikation vor. Darum schauen wir uns dessen Funktionalität nun genauer an. 3.2.2.1
Automatisches Laden der Klassen
Ein nicht ganz so idealer Aspekt der Bootstrap-Datei in der originalen Hello WorldApplikation ist, dass es eine Menge Aufrufe für Zend_Loader::loadClass() gibt, um die erforderlichen Klassen zu laden, bevor wir sie verwenden können. In größeren Applikationen arbeitet man sogar mit noch mehr Klassen (was in der ganzen Applikation dann für Durcheinander sorgt), bloß um sicher zu sein, dass die richtigen Klassen zur richtigen Zeit eingebunden werden. Für unsere Places-Website nehmen wir __autoload() von PHP, sodass PHP die Klassen automatisch lädt. PHP5 hat die magische Funktion __autoload() eingeführt, die immer aufgerufen wird, wenn man eine Klasse zu instanziieren versucht, die bisher noch nicht definiert wurde. Die Klasse Zend_Loader hat eine spezielle Methode registerAutoload(), die extra mit __autoload() eingesetzt wird (siehe Listing 3.1 n). Diese Methode wird automatisch die Funktion spl_autoload_register()der Standard PHP Library (SPL) von PHP5 verwenden, sodass mehrere Autoloader eingesetzt werden können. Nachdem Zend_Loader::registerAutoload() aufgerufen worden ist, wird die Datei, die die Klasse enthält, eingebunden, sobald eine Klasse instanziiert wird, die noch nicht definiert wurde. Das löst das Problem mit dem Wirrwarr von Zend_Loader::loadClass() und gewährleistet, dass nur die für eine bestimmte Anfrage benötigten Dateien geladen werden. 3.2.2.2
Die Konfiguration mit Zend_Config
Es ist eine implizite Anforderung, dass die Daten der Places-Website in einer Datenbank wie MySQL gespeichert werden. Wir müssen die Verbindungseinstellungen für die Datenbank speichern, und zu diesem Zweck arbeiten wir mit Zend_Config. Bei Zend_Config gibt es drei Formate für Konfigurationsdateien: XML, INI und PHP-Arrays. Wir nehmen das INIFormat, weil es einfach zu pflegen ist. enthält ein objektorientiertes Interface zu den Konfigurationsdaten, ungeachtet des geladenen Dateiformats. Nehmen wir eine INI-Datei namens config.ini, die die folgenden Informationen enthält:
Ist diese Datei geladen, kann auf die Daten wie folgt zugegriffen werden: $config = new Zend_Config_Ini('config.ini', 'db'); $adapter = $config->adapter; $databaseHost = $config->database->host;
Beachten Sie, dass der Punkt innerhalb des Schlüsselnamens (.) einer Einstellung von Zend_Config automatisch in einen hierarchischen Separator umgewandelt wird. Somit können wir die Konfigurationsdaten innerhalb der Grenzen des INI-Dateiformats ganz leicht gruppieren. Eine andere Erweiterung für das INI-Format, die Zend_Config unterstützt, ist die Abschnittsvererbung, wobei der Doppelpunkt als Separator (:) innerhalb des Abschnittsnamens verwendet wird. Somit können wir einen Basissatz von Konfigurationseinstellungen definieren und dann spezielle Änderungen für verschiedene übergeordnete Sektionen anbieten (siehe Listing 3.2). Listing 3.2 Beispiel einer Zend_Config-INI-Datei [general] db.adapter = PDO_MYSQL db.params.host = localhost db.params.username = user1234 db.params.password = 1234 db.params.dbname = db_one [live : general] db.params.host = livedb.example.com
Wir können diese INI-Datei nun auf zweierlei Weise laden: Auf der Live-Site wird die INI-Datei mit diesem Befehl geladen, der die Live-Konfiguration lädt: $config = new Zend_Config_Ini('config.ini', 'live');
Um die dev-Konfiguration zu laden, damit sie auf der lokalen Workstation eines Entwicklers genutzt werden kann, müsste die folgende Ladeanweisung verwendet werden: $config = new Zend_Config_Ini('config.ini', 'dev');
In beiden Fällen wird Zend_Config zuerst den allgemeinen Abschnitt laden und dann die Settings im live- oder dev-Abschnitt anwenden. Das hat den Effekt, dass abhängig davon, ob die Applikation auf dem Live- oder dem Entwicklungsserver läuft, jeweils eine andere Datenbank ausgewählt wird.
68
3.2 Die ersten Programmzeilen 3.2.2.3
Die Konfigurationsdatei für Places
Weil wir für die Places-Website nur eine Konfigurationsdatei haben, werden wir sie config.ini nennen (siehe Listing 3.3) und sie im application-Verzeichnis ablegen. Listing 3.3 Die anfängliche config.ini-Datei für Places: application/config.ini [general] db.adapter = PDO_MYSQL db.params.host = localhost db.params.username = zfia db.params.password = 123456 db.params.dbname = places date_default_timezone = "Europe/London" [live : general]
An diesem Punkt hier brauchen wir nur die Verbindung zur Datenbank einzurichten. Der Testabschnitt dient dem automatischen Testen unserer Applikation. Während wir testen, soll die Hauptdatenbank nicht angerührt werden. Also nehmen wir eine separate Datenbank, die wir beim Testen verschiedener Szenarien nach Belieben überschreiben können. Der Konfigurationsabschnitt aus der Datei config.ini wird in der Bootstrap-Klasse geladen (Listing 3.1 o), und dann wird das daraus resultierende Zend_Config-Objekt in der Zend_Registry-Klasse gespeichert, damit die Konfigurationsdaten ausgelesen werden können, wann immer wir sie brauchen. 3.2.2.4
Zend_Registry
Die Nutzung globaler Variablen für große Applikationen wird allgemein als unklug betrachtet, weil dadurch eine Kopplung zwischen den Modulen eingeführt wird und man nur sehr schwer nachverfolgen kann, wo und wann eine bestimmte globale Variable während der Verarbeitung eines Skripts modifiziert wird. Die Lösung besteht darin, Variablen als Parameter durch Funktionen dorthin zu übergeben, wo die Daten benötigt werden. Das verursacht insofern ein Problem, dass Sie am Ende eine ganze Menge von durchgereichten Parametern haben könnten, die die Funktionssignaturen vollstopfen, die mit diesen Daten nichts anderes machen als sie einfach nur an die nächste Funktion weiterzureichen. Eine Lösung besteht darin, die Speicherung solcher Daten in ein Objekt zu konsolidieren, das einfach zu finden ist. Das nennt man auch die Registry. Wie der Name schon nahelegt, implementiert Zend_Registry das Designpattern Registry und ist von daher ein praktischer Platz, um Objekte zu speichern, die in verschiedenen Teilen der Applikation benötigt werden. Mit Zend_Registry::set() werden die Objekte in der Registry gespeichert. Intern wird die Registry als assoziatives Array gespeichert. Wenn Sie also ein Objekt darin registrieren, müssen Sie den Schlüsselnamen angeben, über den sie es identifizieren wollen. Um
69
3 Websites mit dem Zend Framework erstellen das Objekt an einer anderen Stelle im Code auszulesen, nehmen Sie Zend_Registry:: get(): Sie geben den Schlüsselnamen an, und eine Referenz auf das Objekt wird zurückgegeben. Es gibt auch eine Hilfsmethode namens Zend_Registry::isRegistered(), mit der Sie prüfen können, ob ein bestimmter Objektschlüssel registriert ist oder nicht. Doch eine kleine Warnung sei hier angebracht: Die Verwendung der Registry ist sehr ähnlich wie die Arbeit mit einer globalen Variable, und es kann eine unerwünschte Kopplung zwischen den registrierten Objekten und dem restlichen Code eintreten, wenn Sie nicht aufpassen. Zend_Registry sollte mit Umsicht eingesetzt werden. Wir werden es ganz sicher bei zwei Objekten nehmen: $config und $db. Diese beiden Objekte sind ideal zum Speichern in einer Registry, weil sie im Allgemeinen nur gelesen und nicht geschrieben werden. Somit können wir ganz sicher sein, dass sie im Verlauf einer Anfrage höchstwahrscheinlich nicht verändert werden. Auch wenn die Daten sich in der Registry befinden, werden wir trotzdem die relevanten Konfigurationsdaten in unserer Applikation weiterreichen, wenn dadurch eine Kopplung minimiert wird oder ein bestimmter Abschnitt einfacher zu testen ist. Nachdem die Konfiguration nun geladen ist, können wir uns den finalen Abschnitt des Bootstrap-Konstruktors in Listing 3.1 anschauen. Durch die Initialisierung der Datenbank ist gewährleistet, dass wir in der ganzen restlichen Applikation von allen Models aus auf die Datenbank zugreifen können. 3.2.2.5
Initialisierung der Datenbank
Wie in Kapitel 2 angesprochen, können wir anhand der Factoryklasse Zend_Db einen Zend_Db_Adapter speziell für unsere Datenbank erstellen. In diesem Fall empfangen wir ein Objekt des Typs Zend_Db_Adapter_Pdo_Mysql. Um das Objekt zu erstellen, müssen wir den Adapternamen und ein Array mit Konfigurationsparametern oder ein korrekt eingerichtetes Zend_Config-Objekt übergeben. Das verschachtelte Parametersystem von Zend_Config wird dann die erwarteten Daten für die Methode Zend_Db::factory() bereitstellen. Wir nehmen in Listing 3.3 den Top-LevelSchlüssel db und die Unterschlüssel adapter, um der Factory zu sagen, welche Instanzen von Zend_Db_Adapter geladen werden sollen, und params, der dem Adapter übergeben wird, um die Verbindung mit der Datenbank aufzubauen. Die in params erforderlichen Schlüssel sind speziell für den jeweils geladenen Adapter, wobei host, dbname, username und password allgemeine Maßgaben für alle Datenbanken sind. Die Datenbankinitialisierung in Bootstrap übergibt das Konfigurationsobjekt $config>db an Zend_Db::factory(), und der zurückgegebene Adapter wird als Standard-Adapter für die Zend_Db_Table-Objekte eingerichtet, die wir verwenden werden (Listing 3.1 p). Es registriert auch den Adapter in der Zend_Registry, sodass wir ihn für Ad-hoc-SQLAbfragen auslesen können.
70
3.3 Die Homepage
3.2.3
Der Start der Applikation
Wir haben nun ein Bootstrap-Objekt erstellt, das die Umgebung initialisiert, und können unsere Applikation starten. Der Code, der mit diesem Objekt arbeitet, befindet sich in public/index.php und wird in Listing 3.4 gezeigt. Listing 3.4 Mit public/index.php wird die Applikation gestartet. runApp();
Wählt den
zu ladenden config-Abschnitt
Startet die
Applikation
Damit wir auch den korrekten Abschnitt laden, nehmen wir die Umgebungsvariable PLACES_CONFIG. Diese wird jeweils pro Server mittels des Apache-Konfigurationsbefehls SetEnv PLACE_CONFIG {section_name} gesetzt. Für einen Entwicklungsserver konfigurieren wir die Apache-Konfiguration mit SetEnv PLACE_CONFIG dev, und bei der Live-Site nehmen wir SetEnv PLACE_CONFIG live. Wenn die Umgebungsvariable nicht existiert, greifen wir auf den allgemeinen Abschnitt zurück n. Der Start der Applikation verläuft einfach über die Instanziierung von Bootstrap und den Aufruf von runApp()o. Die Methode configureFrontController() (Listing 3.1 p) richtet den Front-Controller in der gleichen Weise wie die Bootstrap-Datei für Hello World ein (Listing 2.1 in Kapitel 2). Wiederum nehmen wir das applications/controllers-Verzeichnis, um die ControllerDateien zu speichern. Die runApp()-Methode ruft schließlich die dispatch()-Methode des Front-Controllers auf, um die Applikation zu starten. Wir haben nun den nötigen Code, um die Website zu starten. Als Nächstes widmen wir uns den Controllern.
3.3
Die Homepage Die Homepage ist das Aushängeschild unserer Applikation. Also sollten wir darauf achten, dass wir eine attraktive Seite mit einer leicht zu nutzenden Navigation haben. Die Seite gliedert sich in vier Hauptbereiche: Ganz oben findet sich die Kopfzeile (Header) mit dem Logo und der Hauptnavigation. Der Inhaltsbereich steht in der Mitte, und eine Spalte auf der rechten Seite bietet Platz für Werbebanner. In der Fußzeile (Footer, in Abbildung 3.2 nicht gezeigt) stehen die Kontakt- und Copyright-Informationen für alle Seiten. Zur Erstellung der Homepage werden wir als Erstes Models und die zugehörigen UnitTests erstellen, mit denen die Models validiert werden sollen. Wir werden die Unit-Tests im Laufe unserer Arbeit schreiben, damit wir beim Refakturieren von Code und dem Einfügen neuer Funktionalitäten sicher sind, dass nichts kaputt geht, von dem wir schon wissen, dass es funktioniert.
71
3 Websites mit dem Zend Framework erstellen Das Kernstück der Places-Website sind die Rezensionen darüber, wohin man gut mit Kindern fahren kann. Also beginnen wir damit.
3.3.1
Die grundlegenden Models
Wir brauchen eine Liste der Rezensionen auf der Homepage. Also fangen wir mit einer Datenbanktabelle namens Reviews an, wobei jede Zeile eine Rezension eines Zielortes darstellt. Jeder Ort soll mehr als eine Rezension bekommen können. Also brauchen wir eine Liste der Zielorte in einer Tabelle namens places. Das anfängliche Datenbankschema ist in Abbildung 3.4 zu sehen. places +id: int +date_created: datetime
Abbildung 3.4 Um die ersten Seiten erstellen zu können, besteht die Datenbank aus zwei Tabellen mit einem Fremdschlüssel von reviews zu places.
Wir können nun weitermachen und die ersten Model-Klassen Reviews und Places erstellen. Damit alles gut organisiert und leicht zu finden ist, speichern wir das Reviews-Model in application/models/Reviews.php und das Places-Model in application/models/Places.php. Die Klassen sind anfänglich sehr einfach: class Reviews extends Zend_Db_Table { protected $_name = 'reviews'; } class Places extends Zend_Db_Table { protected $_name = 'places'; }
Wie bereits in Kapitel 2 erläutert, bekommen wir durch Zend_Db_Table die gesamte Funktionalität eines Table-Data-Gateway-Patterns, ohne irgendwelchen Code schreiben zu müssen. Somit können wir aus der Datenbanktabelle lesen, schreiben und löschen, indem Methoden in der übergeordneten Klasse aufgerufen werden. Wir wollen natürlich keinen Code nur für die Schublade schreiben. Also werden wir unsere Model-Klassen erst dann erweitern, wenn wir es müssen. Momentan werden wir die Datenbank direkt mit ein paar Zeilen füllen, damit wir etwas haben, was auf der Homepage dargestellt werden kann (Listing 3.5).
72
3.3 Die Homepage Listing 3.5 MySQL-Anweisungen um die Tabellen zu erstellen und mit ersten Daten zu füllen CREATE TABLE places ( id int(11) NOT NULL auto_increment, date_created datetime NOT NULL, date_updated datetime NOT NULL, name varchar(100) NOT NULL, address1 varchar(100) default NULL, address2 varchar(100) default NULL, address3 varchar(100) default NULL, town varchar(75) default NULL, county varchar(75) default NULL, postcode varchar(30) default NULL, country varchar(75) default NULL, PRIMARY KEY (`id`) );
Inkrementiert Primärschlüssel automatisch
INSERT INTO places (name, address1, town, county, postcode, date_created) VALUES ('London Zoo', 'Regent\'s Park', 'London', '', 'NW1 4RY', '2007-02-14 00:00:00') ,('Alton Towers', 'Regent\'s Park', 'Alton', 'Staffordshire', 'ST10 4DB', '2007-02-14 00:00:00') ,('Coughton Court', '', 'Alcester', 'Warwickshire', 'B49 5JA', '2007-02-14 00:00:00'); CREATE TABLE reviews ( id int(11) NOT NULL auto_increment, date_created datetime NOT NULL, date_updated datetime NOT NULL, place_id int(11) NOT NULL, user_name varchar(50) NOT NULL, body mediumtext NOT NULL, rating int(11) default NULL, helpful_yes int(11) NOT NULL default '0', helpful_total int(11) NOT NULL default '0', PRIMARY KEY (`id`) );
Erstellt Fremdschlüssel für Places-Tabelle
INSERT INTO reviews (place_id, body, rating, date_created) VALUES (1, 'The facilities here are really good. All the family enjoyed it', 4, '2007-02-14 00:00:00') ,(1, 'Good day out, but not so many big animals now.', 2, '2007-02-14 00:00:00') ,(1, 'Excellent food in the cafeteria. Even my 2 year old ate her lunch!', 4, '2007-02-14 00:00:00');
Das SQL in Listing 3.5 muss über einen MySQL-Client wie die Befehlszeilenapplikation mysql oder eine andere wie phpMyAdmin in die Datenbank eingelesen werden. Wenn Sie mit einem anderen Datenbankserver arbeiten, muss das SQL natürlich entsprechend angepasst werden. Da wir nun die Daten in der Datenbank haben, können wir mit dem PHP-Unit-Framework Tests schreiben, um zu prüfen, ob die Models wie gewünscht funktionieren.
73
3 Websites mit dem Zend Framework erstellen
3.3.2
Tests der Models
In Abschnitt 3.1 wurde schon davon gesprochen, welche Bedeutung Tests für den Code haben. Über Unit-Tests stellen wir sicher, dass der Code so wie gewünscht funktioniert. Unit-Testing ist der Prozess, einzelne Funktionalitäten separat vom restlichen Code zu prüfen. Die Tests müssen schnell durchzuführen sein, weil wir sie immer dann machen wollen, wenn wir am Code etwas geändert haben, um sicher zu sein, dass der Code korrekt funktioniert. So haben wir ein Sicherheitsnetz, wenn wir zur Verbesserung des Programms Änderungen vornehmen. Wenn die Tests weiterhin funktionieren, waren die Änderungen okay. Wenn die Tests nicht mehr funktionieren, können die Änderungen zurückgenommen werden. Der Prozess, einen bereits funktionierenden Code zu verändern, um ihn zu verbessern und leichter verständlich zu machen, nennt man Refaktorierung (refactoring). Dieser Vorgang ist eine sehr wichtige Fähigkeit und dabei erforderlich, um eine Applikation über einen langen Zeitraum warten und pflegen zu können. Eine Refaktorierung ohne Testläufe, ob bei den Überarbeitungen etwas kaputtgegangen ist, kann zu einem höchst riskanten Unterfangen werden. Deswegen wird sie manchmal nicht durchgeführt, und der Quellcode der Applikation wird schwerer verständlich. Mit Tests wird die Refaktorierung leicht und macht vielleicht sogar Spaß – alles wird anders, weil Sie in Ihren Code Vertrauen haben können. Weil wir für den Großteil unseres Codes Tests schreiben werden, brauchen wir ein System, um die Tests durchführen zu können. PHPUnit (http://www.phpunit.de) ist ein Test-Framework für PHP, das jeden Test isoliert durchführt und mit dessen Funktionalität der Test-Code strukturiert werden kann. Anmerkung
Um Unit-Tests durchführen zu können, müssen Sie PHPUnit installieren. Die Anweisung dafür finden Sie auf der Website von PHPUnit (http://www.phpunit.de).
Listing 3.6 zeigt den ersten Test unserer Models, obwohl es in dieser Phase noch nicht viel zu testen gibt! Listing 3.6 Ein Unit-Test für das Places-Model: tests/Models/PlacesTest.php
74
3.3 Die Homepage public function testFetchAll() { $placesFinder = new Places(); $places = $placesFinder->fetchAll(); $this->assertSame(3, $places->count());
Prüft, ob
Zeilenanzahl korrekt ist
} }
Ein Testfall von PHPUnit besteht aus verschiedenen separaten Methoden, die jeweils einen Test enthalten. Diese Methoden fangen alle mit dem Wort test an, damit PHPUnit sie identifizieren kann. Die setUp()-Methode n wird vor jedem Test gestartet und kann darum verwendet werden, um das System für den Test zu initialisieren. Darüber wird sichergestellt, dass Tests nicht von anderen Tests abhängen und dass ein Test nicht für nachfolgende Tests Probleme verursacht. Die Methode tearDown() wird nach jedem Test gestartet und führt somit möglicherweise erforderliche Aufräumarbeiten durch. Wir benennen die Testklasse anhand der gleichen Konvention wie bei den Klassen im Zend Framework und nehmen den Namen des Unterverzeichnisses (mit Unterstrichen getrennt) auf. In diesem Fall heißt der Test models_PlacesTest, weil er in der Datei models/PlacesTest.php innerhalb des Verzeichnisses tests gespeichert ist. Der Test wird über das Befehlszeilen-phpunit-Skript aus dem tests-Verzeichnis gestartet: phpunit models_PlacesTest
Zum Testen unseres Models soll unsere Datenbank für jeden Test in einem bestimmten, bekannten Zustand sein. Wir haben eine separate Klasse namens TestConfiguration geschrieben, die dafür die Methode setupDatabase() enthält. Sie ist nicht Teil dieser Klasse, weil sie auch zum Testen der anderen Models verwendet wird. Unser erster Test testFetchAll() führt einen einfachen Plausibilitätstest durch, um zu sehen, ob auch alles funktioniert o. Dabei werden alle Zielorte aus unserer Testdatenbank gesammelt und gezählt. In diesem Fall erwarten wir drei Zielorte, und in Abbildung 3.5 ist ersichtlich, dass genau das auch ausgegeben wird. Natürlich garantieren die eigenen UnitTests des Zend Frameworks, dass die Methode fetchAll() funktioniert. Also werden Sie bei Ihren eigenen Applikationen nur die Funktionalität testen, die Sie selbst erstellt haben.
Abbildung 3.5 Die Unit-Tests werden von der Befehlszeile aus mit dem Skript phpunit gestartet. Dieses Skript gibt die Anzahl der Tests und die Details etwaiger Fehler aus.
PHPUnit ordnet den Namen des Tests der korrekten Datei zu, startet ihn und gibt eventuell vorkommende Fehler aus. Das bedeutet, wir können jede Testklasse isoliert starten. Generell werden alle Tests so oft wie möglich durchgeführt, damit wir sicher sind, in der Applikation keine Rückschritte zu machen. Dafür müssen wir die Tests in eine Hierarchie von Testsuiten organisieren.
75
3 Websites mit dem Zend Framework erstellen 3.3.2.1
Die Strukturierung von Tests
Damit unser Testfall funktioniert und um mehrere Klassen von Testfällen zu unterstützen, werden wir unsere Tests in zwei Suiten organisieren: eine für Models und eine für Controller. Diese Suiten gruppieren wir in einer umfassenden Suite, damit wir alle Tests mit dem einfachen Befehl phpunit AllTests.php starten können. Ein gewisser Initialisierungsaufwand ist dabei erforderlich, also werden wir das in einer separaten Datei namens TestConfiguration.php ablegen (Abbildung 3.6).
Abbildung 3.6 Die Unit-Tests werden in Controller und Models aufgeteilt. Mit den Skripten controllers/AllTests.php und models/AllTests.php können wir bei Bedarf jede Testgruppe unabhängig von der anderen laufen lassen.
Die Datei TestConfiguration.php enthält die Klasse TestConfiguration mit den beiden Methoden setup() und setupDatabase() (Listing 3.7). Listing 3.7 Die Klasse TestConfiguration
Startet Setup-Funktion,
wenn Datei benötigt wird
class TestConfiguration { static function setup() { $lib = realpath(dirname(__FILE__) . '/../../../lib/'); set_include_path(get_include_path() . PATH_SEPARATOR . $lib);
Legt Zend Framework-Library im Pfad ab
require_once dirname(__FILE__) . '/../application/bootstrap.php'; self::$bootstrap = new Bootstrap('test');
Lädt Bootstrap
über Testkonfiguration
} static function setupDatabase() { $db = Zend_Registry::get('db'); $db->query(<<<EOT DROP TABLE IF EXISTS places; EOT ); $db->query(<<<EOT CREATE TABLE places ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY , date_created DATETIME NOT NULL ,
Die Methode setUp() wird gestartet, wenn die Datei TestConfiguration.php eingebunden wird n. Sie richtet den include-Pfad ein o, damit das Zend Framework in den Pfad aufgenommen wird. Dann richtet sie mittels der Bootstrap-Klasse aus Listing 3.1 die Applikation anhand des Testabschnitts aus der Datei config.ini ein p. Wir arbeiten mit diesem Testabschnitt, damit wir eine andere Datenbank (places_test) angeben können. So sind wir sicher, dass wir nicht aus Versehen etwas an unserer Hauptdatenbank verändern. Durch die Methode TestConfiguration::setupDatabase()q gehen wir sicher, dass wir jeden Test mit einer sauberen, „frischen“ Datenbank beginnen. Diese Funktion ist einfach ein Satz mit SQL-Abfragen, die die Tabelle löschen, erneut erstellen und sie mit den gleichen Daten füllen. Das bedeutet, dass Sie bei allen Tests sicher sein können, dass sich die Datenbank im korrekten Zustand befindet. Der mit dem Buch ausgelieferte Quellcode enthält die Dateien für die Testsuite und den kompletten SQL-Code, der die Datenbank zurücksetzt. Da die Models nun definiert und getestet sind, wenden wir unsere Aufmerksamkeit dem Controller-Teil des MVC-Systems zu: Nun erstellen wir den Homepage-Controller und aktualisieren das Places-Model, um die erforderlichen Daten bereitstellen zu können
3.3.3
Der Homepage-Controller
Die Action des Homepage-Controllers ist die Standard-Action des Standard-Controllers. Also wird der URL /index/index auch die Homepage darstellen, genau wie der URL /.
77
3 Websites mit dem Zend Framework erstellen Jeder URL mappt die Methode indexAction() mit der Klasse IndexController. In dieser Methode müssen wir eine Liste der zuletzt aktualisierten Zielorte aus dem Places-Model sammeln und sie der View zuweisen. Die View wird sich dann darum kümmern, wie diese Inhalte für den Anwender dargestellt werden. 3.3.3.1
Die Aktualisierung des Places-Models
In Zend_Db_Table ist die Funktion fetchAll() enthalten, mit der man mehrere Zeilen aus der Datenbank auslesen kann. Also können wir sie direkt aus dem Controller heraus aufrufen. Doch wir werden die Anforderungen für die Liste der Homepage innerhalb der Klasse Places kapseln. So bleibt der Datenbankcode dort, wo er hingehört: im Model. Außerdem bekommen wir so eine Klasse, der wir weitere Funktionalitäten hinzufügen können, die für das Konzept der Zielorte spezifisch sind. Wir fügen der Klasse Places eine neue Funktion fetchLatest() hinzu, die automatisch die Resultate (umgekehrt nach dem Datum der Aktualisierung) ordnet – siehe Listing 3.8. Listing 3.8 fetchLatest() garantiert, dass die Business-Logik in der Model-Schicht bleibt. class Places extends Zend_Db_Table { protected $_name = 'places'; function fetchLatest($count = 10) { return $this->fetchAll(null, 'date_created DESC', $count); } }
Gibt Namen der Datenbanktabelle an
Ordnet umgekehrt nach Datum, begrenzt auf $count
Durch diese einfache Funktion werden die Daten korrekt sortiert, und der Aufrufer kann angeben, wie viele Einträge er auslesen will. Eine gute Faustregel lautet, dass man mindestens einen Test für jede Funktionalität schreiben sollte, durch die man das Model erweitert. Das bedeutet, dass Sie Tests für jedes diskrete Verhalten der Model-Klasse schreiben. Ein Verhalten kann über verschiedene Methoden implementiert werden und einfach eine Verwendung nur einer Methode beschreiben, die mehrere Argumente akzeptiert. In diesem Fall ergänzen wir die Datei tests/models/PlacesTest.php um einen Test für fetchLatest() (siehe Listing 3.9.)
78
3.3 Die Homepage Listing 3.9 Der Testfall für die Methode fetchLatest() class Models_PlacesTest extends PHPUnit_Framework_TestCase { // ... public function testFetchLatestShouldFetchLatestEntriesInReverseOrder() { Ruft zu testende $placesFinder = new Places(); Funktion auf $places = $placesFinder->fetchLatest(1); $this->assertSame(1, $places->count()); $thisPlace = $places->current(); $this->assertSame(2, (int)$thisPlace->id);
Testet Limit Testet Sortierung
} }
Diese Testmethode prüft die beiden Features der fetchLatest()-Methode. Zuerst prüfen wir, ob die korrekte Anzahl der Einträge zurückgegeben wird n, dann deren Reihenfolge, indem wir darauf achten, dass der korrekte Eintrag zurückgegeben wird o. Der zweite Test zeigt, warum wir unseren Datensatz kontrollieren und darauf achten müssen, dass Tests nicht miteinander interagieren. Ich muss die Testdaten so einrichten, dass bei der zweiten Datenbankzeile mit der ID 2 das Erstellungsdatum (date_created) das neueste ist. Würde das verändert, dann würde der Test fehlschlagen. Wir haben unser Model für die Daten erstellt, die auf der Homepage dargestellt werden, und so können wir uns nun an die Controller-Funktion selbst machen. 3.3.3.2
Die Controller-Action
Die Methode indexAction() des Controllers muss darauf achten, dass die View alles enthält, was für den Anwender dargestellt werden soll. Beachten Sie, dass es sich dabei um die für den Hauptinhalt dieser Seite spezifischen Daten handelt und es also nicht um Daten geht, die an anderer Stelle erstellt werden (z. B. die Werbung oder die Daten für die Kopfoder Fußzeile). Bezeichnungen für die Testmethoden Die Namen der Tests sollten das zu testende Verhalten beschreiben. Wie Sie sehen, nennen wir den Test testFetchLatestShouldFetchLatestEntriesInReverseOrder(). Das ist zwar ganz schön lang und nicht ganz einfach zu lesen, aber PHPUnit verfügt über ein Feature namens testdox, das die mit Binnenmajuskeln (CamelCase) geschriebenen Wörter in separate Wörter übersetzt. So lesen Sie am Ende eine ganz schöne Spezifikation. Eine gut benannte Testmethode macht auch viel deutlicher, welches spezielle Verhalten in der Methode getestet wird, was wiederum Refaktorierung und Bugfixing vereinfacht. PHPUnit unterstützt auch die Verwendung von DocBlock-Annotationen. Eine hilfreiche Annotation ist @group, die zum Gruppieren von zusammengehörigen Tests verwendet werden kann. Sie wird oft für Metadaten benutzt, also die Aufzeichnung von BugNummern, damit der Name des Tests das Verhalten im Test beschreiben kann.
79
3 Websites mit dem Zend Framework erstellen Die Funktion fetchLatest() gibt einen Zend_Db_Table_Rowset zurück, über den anhand des foreach()-Konstrukts in unserem View-Skript iteriert werden kann. In Listing 3.10 weisen wir ihn der View innerhalb von indexAction() zu. Listing 3.10 indexAction() gibt eine Liste der kürzlich hinzugefügten Zielorte aus. class IndexController extends Zend_Controller_Action { public function indexAction() Gibt Titel für { Browser-Titelzeile an $this->view->title = 'Welcome'; $placesFinder = new Places(); $this->view->places = $places->fetchLatest(); Weist der View } kürzlich ergänzte }
Zielorte zu
Die indexAction-Methode des IndexControllers ist recht einfach. Sie macht nichts anderes, als der View einige Membervariablen zuzuweisen. Das Model hat alle Details des Umgangs mit der Datenbank gekapselt und es dem Controller überlassen, auf der Ebene der Business-Logik zu arbeiten. So arbeitet die MVC-Separierung, und der Controller muss sich nur darum kümmern, dass die Model-Daten mit der View verknüpft werden und die View sich selbst rendern lassen. Wie beim Model müssen wir nun Unit-Tests für den Controller schreiben, damit wir sicher sein können, dass die Site auch bei späterem Wachstum immer funktionsfähig bleibt. 3.3.3.3
Den Controller testen
Um unsere ersten Arbeiten am Homepage-Controller abzuschließen, schreiben wir einen Test, um dessen Funktionieren zu prüfen. Das machen wir in der Datei tests/controllers/ IndexControllerTest.php. Das Unterverzeichnis des Controllers im tests-Verzeichnis erlaubt es uns, die Tests in der gleichen Weise zu trennen, wie wir das mit dem Applikationscode machen. Wir erstellen eine neue Testfall-Klasse namens controllers_IndexControllerTest (siehe Listing 3.11). Listing 3.11 Test des index-Controllers bootstrap = array($bootstrap, 'configureFrontController'); parent::setUp();
80
Setzt die Initialisierungsmethode des Front-Controllers
3.3 Die Homepage } public function testHomePageIsASuccessfulRequest() { Startet Applikation $this->dispatch('/');
$this->assertNotRedirect(); } public function testHomePageDisplaysCorrectContent() { // set up request $this->dispatch('/'); // test the output $this->assertQueryContentContains('h1', 'Welcome'); $this->assertQueryContentContains('h2', 'Recent reviews'); $this->assertQueryCount('td a', 3); $this->assertQueryContentContains('td a', 'Alton Towers');
Prüft, ob auf Error-Handler umgeleitet wird
Prüft Daten die der
View zur Darstellung zugewiesen werden
} }
Zum Glück vereinfacht die Zend Framework-Klasse Zend_Test_PHPUnit_Controller TestCase die Tests von Controllern sehr. Diese Klasse kümmert sich um die Einrichtung des Front-Controllers, mit dem man dann die spezielle Funktionalität des Controllers testen kann. In unserem Fall wollen wir testen, ob die Homepage keinen Fehler produziert (und arbeiten dafür mit testHomePageIsASuccessfulRequest()) und dass sie die erwarteten Inhalte produziert (dafür nehmen wir testHomePageDisplaysCorrectContent()). Wir müssen darauf achten, dass der Front-Controller bei jedem Test im Testfall immer auf die gleiche Weise eingerichtet wird. Das besorgt die übergeordnete Methode setUp(), doch bevor wir sie aufrufen, müssen wir eine Methode bereitstellen, um den FrontController zu konfigurieren, nachdem er eingerichtet wurde. Unsere Bootstrap-Klasse in Listing 3.1 enthält eine Funktion namens setupFrontController(), die genau das ist, was wir hier brauchen. Wir verwenden sie, indem wir die lokale Member-Variable-Bootstrap auf ein Array setzen, das die Instanz der Bootstrap-Klasse enthält, die wir in der Methode setup() der TestConfiguration erstellt haben, und den Namen der Methode von Bootstrap, die aufgerufen werden soll n. Der erste Test testHomePageIsASuccessfulRequest() untersucht, ob die Anfrage erfolgreich war. Zuerst nehmen wir die Methode dispatch(), um die Applikation mit der /URL zu starten o. Diese Methode imitiert, wie der Anwender zur Homepage navigiert. Dann testen wir, ob keine Exceptions vorgekommen sind, indem wir uns vergewissern, dass das Antwortobjekt keine Exceptions gespeichert hat p. Auch wird über den integrierten Test assertNotRedirect() gecheckt, ob das Error-Handler-Plug-in die Seite nicht auf eine Fehler-Action umgeleitet hat q.
81
3 Websites mit dem Zend Framework erstellen Der zweite Test für die Homepage testHomePageDisplaysCorrectContent() prüft, ob die Seite die erwarteten Daten darstellt. Wir nutzen die assertQuery-Methodenfamilie, die im Zend_Test_PHPUnit_ControllerTestCase integriert ist. Durch diese Methoden erfahren wir anhand CSS-ähnlicher Spezifikationen, wo ein bestimmter Text zu finden ist r. In der ersten Annahme (assertion) prüfen wir, dass das h1-Tag das Wort „Welcome“ enthält. Die anderen Annahmen prüfen, dass die anderen Teile des View-Skripts wie erwartet gerendert werden. Damit ist der Testlauf für den Homepage-Controller abgeschlossen. Nach Fertigstellung dieses Tests können wir nun selbstsicher weiterarbeiten.
3.4
Zusammenfassung In diesem Kapitel wurde eine echte Applikation namens Places to take the kids! eingeführt, die im restlichen Buch immer weiter ausgebaut wird, während wir alle Komponenten des Zend Frameworks erläutern. In Übereinstimmung mit einigen modernen Designpraktiken versuchten wir nicht, Places zuerst einmal vorab zu gestalten, sondern legten uns eine Story in umgangssprachlichen Formulierungen zurecht, die beschreibt, worum es auf dieser Site geht. Davon leiteten wir eine erste Liste von funktionalen Ideen ab. Anschließend konzentrierten wir uns auf das Model und den Controller für die Homepage und brachten Unit-Tests an Ort und Stelle. Die Testläufe sind absolut unverzichtbar – wir brauchen die Freiheit, unseren Code ändern zu können, um ihn für jedes Stück einer neu hinzugefügten Funktionalität passend zu machen und um sicher zu sein, dass vorhandener Code weiterhin funktioniert! Wir richteten die Verzeichnisstruktur für Places ein und erstellten das erste, erforderliche Model her. Aus Gründen der Einfachheit erweitert unser Model direkt Zend_Db_Table, um den Datenbankzugriff zu ermöglichen. Außerdem haben wir hier auch die Business-Logik implementiert, die zum Auslesen der zuletzt aktualisierten Zielorte erforderlich ist. Wir sind nun soweit, uns detailliert die View-Komponenten des Zend Frameworks anzuschauen, die das Designpattern Composite-View einführen, mit dem wir bei der Website ein allgemeines Look & Feel erstellen und bewahren können.
82
4 4
Die View erstellen
Die Themen dieses Kapitels
Das Designpattern Composite-View Die Arbeit mit der Zend_Layout-Komponente Plug-in für den Front-Controller zur Initialisierung der View erstellen Saubere Skripte mit View-Hilfsklassen In Kapitel 3 ging es um die Erstellung der Website Places to take the kids!, anhand derer wir im Verlaufe dieses Buches die Features des Zend Frameworks vorstellen und demonstrieren, wie Komponenten in eine vollständige Webapplikation integriert werden. Wir schauten uns die Verzeichnisstruktur an, erstellten ein Model und einen Controller für die Homepage und testeten es insgesamt. Um für die Website die erste Phase abzuschließen, müssen wir nun View-Skripte erstellen. Die View ist der Bereich Ihrer Applikation, den die Anwender zu Gesicht bekommen. Darum ist es so wichtig, dass Sie ihn richtig hinbekommen. Bis auf ganz kleine Websites gibt es bei jeder Website darzustellende Elemente, die auf allen Seiten gleich sind. Das sind meistens Kopf- und Fußzeile (Header und Footer) sowie die Navigationselemente, aber auch Werbebanner und andere Elemente können dazugehören, wenn diese vom Design der Site her erforderlich sind. Das Zend Framework enthält eine Suite mit Komponenten, mit denen der sichtbare Teil Ihrer Website leistungsfähig und flexibel, aber auf lange Sicht auch einfach zu warten ist. Wir haben uns die Grundlagen von Zend_View bereits in den Kapiteln 2 und 3 angeschaut. In diesem Kapitel wird es mehr im Detail um die im Framework enthaltenen fortgeschrittenen View-Hilfsklassen gehen. Wir beschäftigen uns auch mit der Komponente Zend_Layout, die eine Two-Step-View enthält, die in ein Composite-View-System integriert ist – so werden sehr flexible Seitendarstellungen möglich. Zuerst soll es aber einmal um das Designpattern Composite-View gehen und warum wir damit arbeiten wollen.
83
4 Die View erstellen
4.1
Die Patterns Two-Step-View und Composite-View Wir müssen sicherstellen, dass die allgemeinen Darstellungselemente auf unserer Website auf allen Seiten einheitlich sind. Also brauchen wir eine Möglichkeit, wie wir es vermeiden können, den immer gleichen HTML-Code kopieren und in die View-Skripte einfügen zu müssen. Ein Weg wäre, jedes View-Skript mit zwei includes zu ergänzen:
page title
body copy here
Das Hauptproblem bei einem solchen Vorgehen ist, dass wir diese beiden Programmzeilen in jedem einzelnen View-Skript wiederholen müssten. Damit wird das View-Skript mit Code vollgestopft, der für die anstehende Aufgabe irrelevant ist. Weil das immer wieder vorkommen wird, ist dieser Bereich des Codes reif für eine Automatisierung. Eine bessere Lösung besteht darin, die allgemeinen Abschnitte automatisch zu separieren. Es gibt zwei Designpattern, die diese Aufgabe abdecken: Two-Step-View und Composite-View. Das Pattern Two-Step-View wird von Martin Fowler wie folgt beschrieben: Two-Step-View kümmert sich um dieses Problem, indem es die Transformation in zwei Phasen aufteilt. In der ersten Phase werden die Model-Daten in eine logische Präsentation ohne irgendwelche Formatierungen transformiert, und in der zweiten Phase wird diese logische Präsentation in die jeweils erforderliche Formatierung konvertiert. Somit können Sie globale Veränderungen vornehmen, indem Sie die zweite Phase abändern, oder Sie können mehrere verschiedene Look & Feels unterstützen, indem für jedes eine eigene zweite Phase integriert wird. (Von http://martinfowler.com/eaaCatalog/twoStepView.html) Die Composite-View wird von Sun wie folgt dokumentiert: [Composite-View] ermöglicht die Erstellung einer zusammengesetzten View, die auf der Inklusion und Substitution modularer dynamischer und statischer TemplateFragmente basiert. Das ermöglicht die Wiederverwendung atomarer Abschnitte der View, indem es ein modulares Design unterstützt. Eine Composite-View kann verwendet werden, um Seiten mit darzustellenden Komponenten zu generieren, die auf verschiedene Weise kombiniert werden können. (Von http://java.sun.com/blueprints/corej2eepatterns/Patterns/CompositeView.html) In Bezug auf die Erstellung einer Website mit dem Zend Framework bedeutet das, dass das an die Action angehängt View-Skript nur das HTML enthalten sollte, das mit dieser Action beschäftigt ist. Die restliche Seite wird unabhängig davon erstellt, und die Inhalte der Controller-Action werden darin platziert. Dies sehen Sie in Abbildung 4.1: Dort wird das Master-Template layout.phtml mit dem allgemeinen Layout dargestellt und ein ActionTemplate mit den auf die Action zutreffenden Inhalten.
84
4.2 Zend_Layout und die Arbeit mit Views Haupt-Template der Site layout.phtml Template mit Action-Inhalten {controller}/ {action}.phtml
Platzhalter z. B. advert/ index.phtml
Abbildung 4.1 Mehrere Templates werden zur Erstellung der Gesamtseite verwendet.
Das Layout-Skript enthält jene Teile der Seite, die nicht direkt mit der aktuellen Action zusammenhängen, also z. B. Kopf- und Fußzeile und Menüabschnitte. Das Action-ViewSkript enthält den für die dispatchte Action spezifischen Darstellungscode. Mit der Zend_ Layout-Komponente des Zend Frameworks wird dieser Prozess bewerkstelligt. Neben der ViewRenderer-Action-Hilfsklasse und der action()-View-Hilfsklasse stellt es ein vollständiges und sehr umfassendes, flexibles zusammengesetzte Darstellungssystem bereit. Der ViewRenderer erwartet, dass alle Action-View-Skripte im Unterverzeichnis views/ scripts gespeichert werden (obwohl das auch anders konfiguriert werden kann). Sie werden je nach Controller weiter in separate Unterverzeichnisse unterteilt. Wie wir in Kapitel 2 gesehen haben, wird die Template-Datei standardmäßig nach der Action benannt und bekommt die Endung .phtml. Somit bekommt das View-Skript der Index-Action den Namen index.phtml und befindet sich im Unterverzeichnis reviews, wenn wir es mit einem Controller namens reviews zu tun haben. Zend_Layout unterstützt dies, indem die LayoutTemplates in einem zentralen layouts-Verzeichnis gespeichert werden. Standardmäßig wäre es das Verzeichnis views/scripts, obwohl es üblich ist, ein separates Verzeichnis wie views/layouts anzugeben.
4.2
Zend_Layout und die Arbeit mit Views Die Komponente Zend_Layout managt das Rendern eines Master-Layout-Skripts, das Platzhalter für die Inhalte zur Einbettung für Inhalte enthält, die von Actions oder anderen View-Skripts generiert werden. Wie bei allen Komponenten des Zend Frameworks arbeitet Zend_Layout in den meisten Fällen mit einem minimalen Konfigurationsaufwand, doch wenn Ihre Anforderungen spezieller sind, ist es auch sehr flexibel. Wird Zend_Layout mit den MVC-Komponenten verwendet, erfolgt die Initialisierung über die Methode startMvc(). Das wird in der Bootstrap-Datei wie folgt erledigt: Zend_Layout::startMvc(array('layoutPath' => '/Pfad/zu/den/Layouts'));
Wie Sie sehen, handelt es sich dabei um eine statische Methode. Also müssen Sie nicht vorher eine Instanz der Zend_Layout-Klasse erstellen. Hinter den Kulissen erstellt startMvc() eine Singleton-Instanz von Zend_Layout und registriert ein Front-ControllerPlug-in und eine Action-Hilfsklasse, die mit der restlichen Applikation verbunden werden
85
4 Die View erstellen können. Das Front-Controller-Plug-in Zend_Layout_Controller_Plugin_Layout hat eine Hook-Funktion namens postDispatch(), die das Layout-Template am Ende der letzten dispatchten Action rendert. Die Action-Hilfsklasse Zend_Layout_Controller_Action_ Helper_Layout wird verwendet, damit man aus einem Controller heraus leicht auf das Zend_Layout-Objekt zugreifen kann. Die Funktion startMvc() akzeptiert ein Array von Optionen, aber erforderlich ist nur layoutPath. Damit wird das Verzeichnis festgelegt, in dem die Layoutdateien gespeichert werden. Standardmäßig wird die Datei layout.phtml von Zend_Layout gerendert. Doch Sie können das in jeder Controller-Methode wie z. B. init() einfach durch folgende Anweisung ändern: $this->_helper->layout->setLayout('layout2');
Das sorgt dafür, dass stattdessen layout2.phtml gerendert wird.
4.3
Die Integration von Zend_Layout in Places Die von Zend_Layout gebotene Funktionalität kommt uns sehr gelegen, also bauen wir sie in die Places-Applikation ein.
4.3.1
Setup
Um Zend_Layout in der Places-Website zu integrieren, müssen wir bei der BootstrapKlasse anfangen. Listing 4.1 zeigt die Änderungen, die wir an der Funktion runApp() vornehmen müssen. Listing 4.1 Zend_Layout in application/bootstrap.php starten public function runApp() { // setup front controller // ... // setup the layout Zend_Layout::startMvc(array( 'layoutPath' => ROOT_DIR . '/application/views/layouts', ));
Setzt layoutPath
auf layouts-Verzeichnis
// ...
Wie in Abschnitt 4.2 erklärt, nutzen wir die Methode startMvc(), um das Zend_LayoutObjekt einzurichten und auch das damit verknüpfte Front-Controller-Plug-in und die Action-Hilfsklasse zu registrieren. Die Standardeinstellungen für Zend_Layout sind für die Places-Site akzeptabel, außer dass wir das Verzeichnis für die Layout-Skripte angeben müssen n. Wir haben uns für views/layouts im application-Verzeichnis entschieden, weil damit die Layoutdateien von den Skriptdateien getrennt bleiben.
86
4.3 Die Integration von Zend_Layout in Places Wir müssen auch unser View-Objekt mit Informationen über die aktuelle Anfrage konfigurieren und die anfänglichen Anforderungen fürs CSS und die Kodierung des Outputs einrichten. Das kann einfach innerhalb der Bootstrap-Klasse erledigt werden, aber um diese Klasse einfacher zu halten und eine Wiederverwendbarkeit zu erleichtern, werden wir ein Front-Controller-Plug-in erstellen, das sich um das View-Setup kümmert. Diese Klasse namens Places_Controller_Plugin_ViewSetup befolgt die Namenskonventionen des Zend Frameworks und wird somit in library/Places/Controller/Plugin/ViewSetup.php gespeichert. Für den Anfang muss die anfallende Arbeit nur einmal erledigt werden, und somit können wir den dispatchLoopStartup()-Hook wie in Listing 4.2 gezeigt verwenden. Listing 4.2 Der Front-Controller Places_Controller_Plugin_ViewSetup class Places_Controller_Plugin_ViewSetup extends Zend_Controller_Plugin_Abstract { /** * @var Zend_View */ protected $_view; public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request) { $viewRenderer = Zend_Controller_Action_HelperBroker:: getStaticHelper('viewRenderer'); $viewRenderer->init(); $view = $viewRenderer->view; $this->_view = $view;
4 Die View erstellen In dieser Methode erledigen wir vier verschiedene Dinge. Zuerst weisen wir der View das Modul, den Controller und die Action aus dem Anfrageobjekt zu n. Weil die DispatchSchleife noch nicht gestartet ist, sind diese Variablen jene, die ursprünglich vom Anwender über den URL angefordert wurden und überschrieben werden, wenn während des Anfragezyklus andere Actions programmatisch eingebunden werden. Wir ergänzen dann die Unterstützung für ein weiteres Verzeichnis für View-Hilfsklassen namens library/Places/View/Helper. Damit haben wir einen Ort, in dem wir ViewHilfsklassen speichern können, die sowohl im Front- als auch dem Admin-Bereich der Website verwendet werden o. Die in library/Places/View/Helper gespeicherten Klassen der View-Hilfsklassen bekommen die Bezeichnung Places_View_Helper_{Hilfsklassenname}, also setzen wir $prefix auf diesen Wert p. Anderenfalls würde die View versuchen, Hilfsklassen über das Standardpräfix Zend_View_Helper zu laden, was bei Klassen im Places-Verzeichnisunterbaum nicht sinnvoll ist. Dann registrieren wir das neue Plug-in beim Front-Controller. Das erfolgt in der Funktion der Klasse Bootstrap in application/bootstrap.php, direkt nachdem wir das Controller-Verzeichnis wie folgt gesetzt haben: runApp()
Schließlich enthält das Zend Framework eine Reihe von View-Hilfsklassen, die bei der Verwaltung des -Abschnitts der Webseite helfen. Diese View-Hilfsklassen sind ungewöhnlich, weil sie zwei Operationsmodi aufweisen, nämlich Setting und Rendering, während die meisten View-Hilfsklassen nur rendern. Wir arbeiten mit der ViewHilfsklasse headMeta(), um den Content Type der Seite zu setzen q, doch damit können auch die Metafelder einer Webseite gesetzt werden, z. B. die Beschreibung und die Schlüsselwörter. Wir nehmen ein anderes „head“-View-Skript headLink(), um die StandardCSS-Datei für die Seite zu speichern, in diesem Fall ist das site.css . . Über die Hilfsklasse headLink() können wir sowohl ein Stylesheet als auch alternative Stylesheets einfügen, falls das gewünscht wird. Nach der Initialisierung des View-Systems können wir uns nun also die Skripte anschauen, die zur Erstellung der Website erforderlich sind.
4.3.2
Layout-Skripte
Das Master-Layout-Skript heißt layout.phtml und wird in view/layouts/layout.phtml gespeichert. Diese Datei dient als Aufbewahrungsort für alle darzustellenden Inhalte. Sie enthält somit nur die fundamentale Struktur des Seitendesigns und delegiert dann den Rest der Inhalte an andere Dateien. Das Master-Layout wird in Listing 4.3 gezeigt.
88
4.3 Die Integration von Zend_Layout in Places Listing 4.3 Das Master-Layout-Skript: view/layouts/layout.phtml
Setzt doctype für Output
doctype(); ?> headMeta(); ?> Gibt für Registry die headTitle(); ?> head-View-Hilfsklassen an headLink(); ?> Rendert Header in separater Datei partial("_header.phtml"); ?> layout()->menu; ?> Rendert Content
Platzhalter für Menü
layout()->content; ?> Rendert Hauptinhalt
für diese Seite
layout()->advert; ?> Rendert Content
Platzhalter für Werbung
partial("_footer.phtml"); ?> Rendert Footer in separater Datei
Wir haben bereits festgelegt, welcher Doctype zu verwenden ist. Darum rufen wir nur die View-Hilfsklasse doctype() auf n, wenn gerendert werden soll, und sie wird den korrekten Doctype ausgeben. So bleibt es uns erspart, danach zu googeln, weil niemand sich das alles merken kann! Die View-Hilfsklasse doctype() unterstützt alle drei HTML-4und alle drei XHTML-1.0-Dokumenttypen, was im Prinzip alle aktuellen Webseiten abdeckt. Wenn HTML 5 veröffentlicht und in Browsern unterstützt wird, können wir davon ausgehen, dass das ebenfalls unterstützt wird. Im -Abschnitt arbeiten wir mit den sogenannten head*-View-Hilfsklassen. Von diesen View-Hilfsklassen gibt es eine ganze Reihe, und wir nutzen drei davon o. Diese wurden anfänglich im Front-Controller-Plug-in Places_Controller_Plugin_ViewSetup gesetzt (Listing 4.2), doch sie können auch während der Bearbeitung der Anfrage im Action-Controller oder einem anderen Plug-in hinzugefügt werden. Die sichtbare Seite ist im -Tag enthalten, und wir haben die Daten noch weiter voneinander separiert, um die Wartbarkeit zu verbessern. Die Kopf- und Fußzeilenabschnitte werden in ihren eigenen Skriptdateien gespeichert und in das Layout über die View-Hilfsklasse partial() für header.phtml p und footer.phtml eingebunden. Die View-Hilfsklasse partial() rendert ein weiteres Skript innerhalb eines „Sandkastens“ (sandbox), in der nur übergebene Variablen verfügbar sind. Damit wird der Geltungsbereich der Version auf nur dieses View-Skript begrenzt, damit man auf alle direkt über $this->variablenName zugegriffen werden kann, und wir können sicher davon ausgehen, dass sie nicht mit Variablen in einem anderen Skript in Konflikt geraten.
89
4 Die View erstellen Eine Anmerkung zu partial() Das in den View-Hilfsklassen partial() oder partialLoop() verwendete View-Skript hat keinen Zugriff auf Variablen, die der View im Controller oder anderen Hilfsklassen und Plug-ins zugewiesen worden sind. Sie müssen alle Variablen, die Sie für das ViewSkript brauchen, als Array an den partial()-Aufruf übergeben – so wie hier: partial('myscript.phtml', array('var'=>$this->var)); ?>
Alle Funktionen der View-Hilfsklassen stehen jedoch ohne weiteren Aufwand zur Verfügung. Die ViewRenderer-Action-Hilfsklasse speicherte den Inhalt aus dem View-Skript der Action in der Antwort in ein benanntes Segment. Mit der View-Hilfsklasse layout() wird dann der Inhalt der benannten Segmente in der Antwort ausgelesen und gerendert. Das stellt den Mechanismus zur Verfügung, mit dem die Darstellung der Action-View-Skripte innerhalb des Master-View-Skripts dargestellt werden kann. Der Standardname für das Antwortsegment lautet content; also können wir das View-Skript der Haupt-Action wie folgt rendern : layout()->content; ?>
Den Namen für den Platzhalter können wir nach Belieben wählen. Bei der Places-Website nehmen wir zwei andere Platzhalter: menu q und advert , die für die Menüdarstellung bzw. die Werbung sorgen. In Abschnitt 4.3.3 wird es darum gehen, wie diese Platzhalter gefüllt werden. Unsere Ausführungen über das Master-Layout-Template aus Listing 4.3 setzen wir damit fort, dass wir View-Skripte anhand von „partials“ separieren. 4.3.2.1
Partielle View-Skripte
Wie wir in Listing 4.3 gesehen haben, nehmen wir zwei View-Skripte innerhalb der partial()-View-Hilfsklassen, damit die Wartung einfacher wird. Die beiden Skripte _header.phtml und _footer.phtml halten einfach das relevante HTML logisch getrennt vom Layout-Template (siehe die Listings 4.4 und 4.6). Listing 4.4 Das header-View-Skript: view/layouts/_header.phtml
Der Header enthält im Wesentlichen mehrere Links zur Accessibility (durch CSS für die meisten Anwender versteckt) und dann die Bilder, die oben auf der Seite verwendet werden. Das alles ist schön sauber in einer eigenen Datei enthalten, damit wir einfach nur den Aufruf von partial() (Listing 4.3 p) ändern müssen, wenn wir beschließen, dass dieser Bereich in verschiedenen Abschnitten der Site unterschiedlich gestaltet werden soll. Um den korrekten Pfad zu den Bilddateien zu erstellen, müssen wir auf den Root-URL der Website verweisen. Das könnten wir direkt im View-Skript erledigen, doch das ist auch allgemein erforderlich und eignet sich somit dazu, in einer View-Hilfsklasse umgesetzt zu werden. Wir werden unsere View-Hilfsklasse baseUrl() nennen. Also lautet deren Klassenname Places_View_Helper_BaseUrl, und sie wird in library/Places/View/Helper/ BaseUrl.php gespeichert (siehe Listing 4.5). Das bedeutet, dass sie im Dispatch-Prozess schon von Anfang an verfügbar ist, wenn wir das Places/View/helper-Verzeichnis dem View-Objekt im Front-Controller-Plug-in ViewSetup in Listing 4.2 hinzufügen. Listing 4.5 Die View-Hilfsklasse Places_View_Helper_BaseUrl class Places_View_Helper_BaseUrl { function baseUrl() { $fc = Zend_Controller_Front::getInstance(); return $fc->getBaseUrl(); Gibt Basis-URL} Wert zurück }
Liest Front-
Controller aus
Die eigentliche Arbeit der Erstellung des korrekten Pfads zum Root-URL des Webservers hat bereits das Anfrageobjekt erledigt, und somit steht der Pfad direkt vom FrontController aus zur Verfügung. Das bedeutet, dass die View-Hilfsklasse BaseUrl sie auslesen und dem View-Skript zurückgeben muss. Auf den Front-Controller kann ganz einfach zugegriffen werden, weil es sich um ein Singleton handelt. Also nehmen wir die statische getInstance()-Methode n und geben einfach das Ergebnis der Elementfunktion getBaseUrl() zurück. Im Vergleich dazu ist das View-Skript für die Fußzeile, _footer.phtml, sehr einfach gehalten (siehe Listing 4.6). Listing 4.6 Das footer-View-Skript: views/layouts/_footer.phtml
Das Footer-Skript enthält jetzt nur den Copyright-Hinweis, aber kann später erweitert werden, um andere Elemente aufzunehmen und die Funktionalität der Site abzurunden.
91
4 Die View erstellen Damit sind die Ausführungen der von unserem Layout erforderlichen View-Skripts abgeschlossen. Wir haben uns das HTML angeschaut, mit dem das äußere Haupt-TemplateSkript erstellt wird. Nun geht es weiter, und wir beschäftigen uns mit den Actions, mit denen der Inhalt für die Platzhalter menu und advert erstellt wird.
4.3.3
Allgemeine Actions anhand von Platzhaltern
Es kommt recht häufig vor, dass man Code auf jeder Seite starten muss – Code, der relativ komplex ist und sich nicht dafür eignet, in einer Hilfsklasse abgelegt zu werden. Bei Places müssen wir das Menü und auch den Bereich für die Werbung auf der rechten Seite erstellen. Dafür ist es ganz naheliegend, mit Actions zu arbeiten, weil sie uns einen Mechanismus bieten, anhand dessen man auf die Models direkt zugreifen kann, und weil sie ein damit verknüpftes View-Skript haben, in dem das erforderliche HTML enthalten ist. Wir müssen diese Actions starten, und unsere erste Lösung besteht darin, dafür die Methode _forward() des Controllers zu nehmen. So können wir am Ende der aktuellen Action eine weitere starten, von der Haupt-Action auf die Action zur Menüerstellung weiterleiten und von dort dann zur Action für die Erstellung des Werbebereichs. Das größte Problem ist, dass wir daran denken müssen, den _forward()-Aufruf am Ende jeder Action in unserer Applikation zu schreiben. Die Entwickler des Zend Frameworks haben dieses Problem vorausgesehen und zur Lösung das Front-Controller-Plug-in ActionStack bereitgestellt. Dessen kompletter Name lautet Zend_Controller_Plugin_ActionStack, und es startet seine interne Liste von Actions. Weitere Actions können ganz einfach hinzugefügt werden, und wie sein Name schon nahelegt, operiert es mit einem auf Stacks basierenden System, bei dem die letzte hinzugefügte Action die erste ist, die gestartet wird. Dieses Prinzip nennt man Last in, first out (LIFO), und damit können wir bei Bedarf die Reihenfolge steuern, in der die Actions gestartet werden sollen. Das Plug-in ActionStack verfügt über einen postDispatch()-Hook, mit dem man der Dispatch-Schleife eine weitere Action hinzufügen kann, wenn aktuell keine Actions laufen. Um den ActionStack anfänglich mit den beiden erforderlichen Actions zu füllen, werden wir unser eigenes Front-Controller-Plug-in namens ActionSetup erstellen. So können wir die Einrichtung des ActionStacks vom Bootstrap-Code getrennt halten. Weil wir dem Stack nur zwei Actions hinzuzufügen brauchen, müssen wir uns auch nur in dispatchLoopStartup() einklinken. Weil ActionSetup in der Datei library/Places/Controller/ Plugin/ActionSetup.php gespeichert wird, lautet der vollständige Klassenname Places_ Controller_Plugin_ActionSetup. Der Code steht in Listing 4.7.
92
4.3 Die Integration von Zend_Layout in Places Listing 4.7 Das Front-Controller-Plug-in Places_Controller_Plugin_ActionSetup class Places_Controller_Plugin_ActionSetup extends Zend_Controller_Plugin_Abstract { public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request) { $front = Zend_Controller_Front::getInstance(); if (!$front->hasPlugin( 'Zend_Controller_Plugin_ActionStack')) { $actionStack = new Zend_Controller_Plugin_ActionStack(); $front->registerPlugin($actionStack, 97); } else { $actionStack = $front->getPlugin( 'Zend_Controller_Plugin_ActionStack'); } $menuAction = clone($request); $menuAction->setActionName('menu') ->setControllerName('index'); $actionStack->pushStack($menuAction); $advertAction = clone($request); $advertAction->setActionName('advert') ->setControllerName('index'); $actionStack->pushStack($advertAction);
Erstellt ActionStack, falls nötig
Fügt menu-Action hinzu Fügt advert-Action hinzu
} }
Das Front-Controller-Plug-in ActionSetup ist sehr einfach und beginnt mit der Initialisierung des ActionStack-Plug-ins, falls erforderlich n. Wir prüfen über die Funktion hasPlugin(), ob es nicht bereits erstellt worden ist, und wenn dem nicht der Fall ist, instanziieren wir es und fügen es am Ende des Plug-in-Stacks ein, damit es so ziemlich nach all den anderen Plug-ins startet (die Plug-ins Zend_Layout und der ErrorHandler kommen danach). Dieser Codeabschnitt ist defensiv, weil wir nicht erwarten, dass der ActionStack schon erstellt worden ist, doch falls wir bei unserer Applikation ein anderes Plug-in eines Drittanbieters einbauen, wollen wir diese Datei nicht erneut bearbeiten müssen. Wie beim Plug-in ViewSetup müssen wir das ActionSetup-Plug-in beim Front-Controller registrieren lassen. Das wird in der runApp()-Funktion der Bootstrap-Klasse erledigt – sofort nach der Registrierung des ActionSetup-Plug-ins: $frontController->registerPlugin( new Places_Controller_Plugin_ActionSetup());
Beachten Sie, dass der postDispatch()-Hook des ViewSetups nach allen anderen Plug-ins außer Zend_Layout starten soll. Also setzen wir das Plug-in explizit auf Position 98. Wir nehmen 98, weil 99 bereits von Zend_Layout verwendet wird und ViewSetup starten soll, direkt bevor die View der Action gerendert wird. Um eine Action im ActionStack einzufügen, klonen wir das aktuelle Anfrageobjekt, und ändern die Kopie entsprechend ab. Für die menu-Action o setzen wir den Actionnamen
93
4 Die View erstellen auf menu und den Controllernamen auf index. Schließlich fügen wir die Action anhand der pushStack()-Methode in den ActionStack ein. Dann wiederholen wir diesen Prozess für die advert-Action p, außer dass wir im Anfrageobjekt einen anderen Actionnamen setzen. Das Schlussergebnis ist, dass die Haupt-Action wie vom Anwender angefordert starten wird, und dann werden alle nachfolgenden Weiterleitungen in dieser Action gestartet. Danach wird dem LIFO-Stack entsprechend die advert-Action starten, anschließend die menu-Action. Wir haben nun erfolgreich den Start der Actions automatisiert, wie es für die Erstellung der gesamten Seite erforderlich ist. Weil es sich bei Places um eine einfache Applikation handelt, gehört die menu-Action zum IndexController, und das Menü selbst ist festkodiert. In einer größeren Applikation ist es wahrscheinlicher, dass das Menü aus einer Datenbank heraus erstellt wird. Weil sie Teil des IndexControllers ist, wird die menuAction()-Funktion in der Datei application/controllers/IndexController.php gespeichert (siehe Listing 4.8). Listing 4.8 Die menuAction()-Funktion des IndexControllers class IndexController extends Zend_Controller_Action { // ... public function menuAction() { Festkodierte Menüelemente $mainMenu = array( array('title'=>'Home', 'url'=>$this->view->url(array(), null, true)), array('title'=>'Browse Places', 'url'=>$this->view->url( array('controller'=>'place', 'action'=>'browse'), null, true)), array('title'=>'Articles', 'url'=>$this->view->url( array('controller'=>'articles'), null, true)), array('title'=>'About', 'url'=>$this->view->url( array('controller'=>'about'), null, true)), ); Weist View zu
Wie Sie sehen, wird durch den Großteil des Action-Codes das $mainMenu-Array erstellt und zum Rendern view n zugewiesen. Das Layout-Skript erwartet, dass sich das Menü in einem Platzhalter namens menu befindet. Um das einzurichten, greifen wir über die _helper-Eigenschaft der Klasse Zend_Controller_Action auf die View-Hilfsklasse View
94
4.3 Die Integration von Zend_Layout in Places Renderer zu und rufen die setResponseSegment()-Methode auf o. Das bedeutet, dass das Action-Skript für diese Action (application/views/scripts/index/menu.phtml) für das menu-Segment des Antwortobjekts gerendert wird, damit es später vom Layout-Skript gesammelt werden kann. Das HTML, mit dem das Menü gerendert wird, steht in den Listings 4.9 und 4.10. Listing 4.9 Das View-Skript des Menüs: application/views/scripts/index/menu.phtml headLink()->appendStylesheet( $this->baseUrl() . '/css/menu.css') ?>
Im View-Skript des Menüs richten wir über die View-Hilfsklasse headLink() ein zusätzliches Stylesheet im Header der Layoutdatei ein n. Das ist möglich, weil das Layout erst dann gerendert wird, nachdem alle Actions gerendert worden sind. Somit können wir den Kopfabschnitt in jeder Action beeinflussen, wenn sie gestartet wird. In diesem Fall müssen wir das Menü stylen, das wir generieren, und das wird am besten über eine separate CSSDatei erledigt, bei der wir wieder die View-Hilfsklasse baseUrl() nehmen, um den Pfad zum Stammverzeichnis der Webseite auszulesen. Das Menü selbst ist recht einfach, und weil wir über einen Satz von Einträgen iterieren, können wir die View-Hilfsklasse partialLoop() ausnutzen o, die das View-Skript _menuItem.phtml für jedes Element des Menüs rendern wird. Dieses View-Skript wird in view/scripts/index/_menuItem.phtml gespeichert und steht in Listing 4.10. Listing 4.10 Ein partielles Skript für Menüelemente
Das für jedes Menüelement gerenderte Template fasst den Titel des Menüs einfach in einen Link n und platziert ihn in das
-Tag, um das Listenelement zu erstellen. Wie üblich nehmen wir die View-Hilfsklasse escape(), um sicher zu gehen, dass alle ungewöhnlichen Zeichen im Titel des Menüs kodiert werden. Die andere Action, die für jede dargestellte Seite gestartet wird, ist die advert-Action. Das funktioniert praktisch auf die gleiche Weise wie die Menü-Action, außer dass sie für den Platzhalter namens advert gerendert wird. Mehr Controller-Logik wird nicht benötigt (siehe Listing 4.11).
95
4 Die View erstellen Listing 4.11 Die advertAction()-Funktion des IndexControllers class IndexController extends Zend_Controller_Action { // ... public function advertAction() { $this->_helper->viewRenderer-> setResponseSegment('advert'); }
Setzt Platzhalter für Antwort
Wir haben im Werbebereich nur eine Grafik. Also steht das HTML komplett im ViewSkript views/scripts/index/advert.phtml (siehe Listing 4.12). Listing 4.12 Das View-Skript der Methode advertAction(): advert.phtml url(array('controller'=>'contact'), null, true); ?>"> Bild dar
Wie Sie sehen, besteht die von uns verwendete Werbung nur aus einem Bild mit einem Link zum Contact-Controller. Mit dem dritten Parameter für die View-Hilfsklasse url() wird die Zusammenstellung zurückgesetzt, damit nur jene Parameter im Array zur Konstruktion des URLs verwendet werden. Wenn wir das nicht machen, dann würde die aktuelle Action bei der Konstruktion des URLs verwendet, und das wollen wir nicht. Alternativ könnten wir die Action in der url()-View-Hilfsklasse selbst setzen. Damit ist die Erstellung des Master-Layout-Templates abgeschlossen. Nun können wir uns dem Inhalt der View der Controller-Action selbst zuwenden. Wie in Kapitel 2 angesprochen, wird das View-Skript für die aktuelle Action in der Datei views/scripts/{ControllerName}/{Actionname}.phtml gespeichert. In Kapitel 3 haben wir die Homepage-Action implementiert (die Index-Action des Index-Controllers), also werden wir nun das damit verknüpfte View-Skript implementieren.
4.3.4
Das View-Skript für die Homepage
Das View-Skript für die Homepage wird in der Datei views/scripts/index/index.phtml gespeichert und ist in Listing 4.13 zu sehen. Listing 4.13 Das View-Skript der Homepage: index.phtml
escape($this->title);?>
Welcome to <em>Places to take the kids! This site will help you to plan a good day out for you and your children. Every place featured on this site has been reviewed by people like you, so you'll be able to make informed decisions with no marketing waffle!
96
Stellt die
Begrüßung dar
4.3 Die Integration von Zend_Layout in Places places)) : ?> Stellt Rezensionen nur dar,
Recent places
wenn welche vorhanden sind
partialLoop('index/_placeRow.phtml', $this->places) ?> Iteriert über Einträge
in $this->places
Im View-Skript für die Homepage fügen wir zuerst den allgemeinen Inhaltstext oben ein n, der den Rahmen für die Website absteckt, und dann präsentieren wir eine Liste der Zielorte, die kürzlich aktualisiert worden sind o. Die Liste verwendet die Elementvariable $places, die in der indexAction() des Controllers in Kapitel 3 (Listing 3.8) eingerichtet wurde. Um über die Elemente im places-Zeilensatz zu iterieren, nutzen wir erneut die View-Hilfsklasse partialLoop, wegen der wir keine foreach()-Schleife benötigen. Jedes Element im Zeilensatz wird in lokale Variablen im View-Skript _placeRow.phtml extrahiert. Beachten Sie, dass wir die Namenskonvention mit einem vorangestellten Unterstrich verwenden, um darauf hinzuweisen, dass es sich bei _placeRow.phtml um ein partielles Skript handelt, das in Verbindung mit einem anderen Skript verwendet wird. Das _placeRow.phtml-Skript wird in Listing 4.14 gezeigt. Listing 4.14 Ein partielles View-Skript für das Homepage-Listing: _placeRow.phtml
url(array('controller'=>'place', Erstellt URL zur 'action'=>'index', Verwendung Link 'id'=>$this->id)); ?>
Das View-Skript _placeRow.phtml erstellt eine Tabellenzeile, die zwei Spalten enthält: den Namen des Ortes und das Datum, wann die Information über den Ort aktualisiert wurde. Wie bei allen Links nehmen wir wieder die View-Hilfsklasse url(), um den Link zu erstellen n. Das erleichtert die Wartung, wenn wir beschließen, selbst erstellte Routen zu nehmen, weil die url()-View-Hilfsklasse weiterhin die korrekten URLs generieren wird. Wir faktorieren den Code zur Datumsformatierung außerdem in eine View-Hilfsklasse o, damit wir ihn auf anderen Seiten erneut verwenden können. Diese View-Hilfsklasse wird in application/views/helpers/DisplayDate.php gespeichert, und der Code steht in Listing 4.15.
97
4 Die View erstellen Listing 4.15 Die View-Hilfsklasse DisplayDate class Zend_View_Helper_DisplayDate { function displayDate($timestamp, $format=Zend_Date::DATE_LONG) { $date = new Zend_Date($timestamp, 'en_GB'); Erstellt Zend_ return $date->get($format); Date-Objekt Liest Datum im } korrekten Format aus }
Beim Erstellen der View-Hilfsklassen ist zu beachten, dass die Groß- und Kleinschreibung der Namen korrekt sein muss. Der Dateiname muss mit einem Großbuchstaben beginnen, der zum Klassennamen passt, aber der Methodenname muss mit einem Kleinbuchstaben anfangen. Das ist ein ganz häufig vorkommendes Problem, das Sie aus dem Konzept bringen kann, wenn Sie auf einem Dateisystem entwickeln, bei dem die Groß/Kleinschreibung egal ist (z. B. NTFS von Windows oder HFS+ von Mac OS XML), und das Deployment dann auf einem Linux- oder Solarissystem vornehmen, das case-sensitive ist, wo also zwischen Groß- und Kleinschreibung unterschieden wird. Für displayDate() instanziieren wir ein Zend_Date-Objekt mit dem erforderlichen Zeitstempel und der Ländereinstellung (locale) n und lesen dann über die Elementfunktion get() die Daten im korrekten Format aus o. Zend_Date ist eine sehr praktische Komponente zum Umgang mit Datums- und Zeitangaben, weil sie auf die Ländereinstellungen des Systems achtet. Wenn wir uns später einmal an die Lokalisierung unserer Applikation in andere Sprachen machen, wird das Datumsformat automatisch auf ein Format geändert, das für die jeweilige Ländereinstellung korrekt ist. Wir haben nun die gesamte View für die Homepage erstellt und uns dafür eines Composite-Ansatzes bedient. Zend_Layout steuert das äußere Template, das alle allgemeinen Elemente für die Site enthält, damit das View-Skript für jede spezifische Action nur von der spezifischen Ausgabe für diese Action betroffen ist. Zend_View enthält ebenfalls einen hilfreichen Satz von View-Hilfsklassen, mit dem wir die Entwicklung beschleunigen können. Mit einige davon haben wir uns schon beschäftigt, als wir Zend_Layout in Places integriert haben, doch nun wollen wir eins nach dem anderen durchgehen, um zu sehen, wofür es gedacht ist und wie es eingesetzt wird.
4.4
Fortgeschrittene View-Hilfsklassen Zur Komponente Zend_View gehören eine ganze Reihe von View-Hilfsklassen, mit denen man die Entwicklung von View-Skripten verbessern kann. Diese View-Hilfsklassen erlauben eine leichtere Integration von Controllern, die Verwaltung der View-Skripte und die Erstellung häufig vorkommender HTML-Kopfzeilen. Wir beginnen mit den ViewHilfsklassen action() und url(), mit denen man die View-Skripte bei Controllern integrieren kann.
98
4.4 Fortgeschrittene View-Hilfsklassen
4.4.1
Die Integration von Controllern
Zwei View-Hilfsklassen werden unterstützt, die mit dem Controller interagieren. Mit action() werden die Actions direkt gestartet, und url() kann URLs generieren, um Controller-Actions zu steuern. 4.4.1.1
Die View-Hilfsklasse action()
Mit action() kann eine Controller-Action aus einem View-Skript heraus gestartet werden. Das Ergebnis kann nach der Rückgabe ausgegeben werden. Damit wird im Allgemeinen genauso gearbeitet wie mit Platzhaltern, um Unterabschnitte der Seiten wiederverwendbar zu rendern. Wir könnten beispielsweise action() nutzen, um die Werbung in layout.phtml mit darzustellen. In Listing 4.3 wurde mit dem folgenden Code die Werbung ausgegeben:
layout()->advert; ?>
Wie in Abschnitt 4.3.3 erläutert, geht dieser Code davon aus, dass die Action IndexController::advertAction() bereits vom ActionStack dispatcht worden ist (siehe Listing 4.7). Als Alternative können wir die action()-View-Hilfsklasse nehmen, um advertAction() direkt in der Datei layout.phtml zu dispatchen – so wie hier:
action('advert', 'index'); ?>
Das für den Anwender im Browser dargestellte Ergebnis ist in beiden Fällen exakt gleich, und es liegt völlig im Ermessen des Entwicklers, ob er sich für die action()-ViewHilfsklasse oder den Layout-Platzhalter entscheidet. Im Allgemeinen hängt es davon ab, wie aktiv das View-Objekt für Sie in der Applikation sein soll. Manche Entwickler betrachten die View als sehr aktiv und wählen somit action(), während andere es eher als passiv behandeln und darauf achten, dass der Controller die Arbeit über das Plug-in ActionStack erledigt. Weil es fundamental wichtig ist, innerhalb eines Template verwendbare Links zu erstellen, schauen wir uns die View-Hilfsklasse url() noch etwas genauer an. 4.4.1.2
Die View-Hilfsklasse url()
Mit url() kann man URL-Strings erstellen, die auf einer benannten Route basieren. Hier ist die Methodensignatur: public function url($urlOptions = array(), $name = null, $reset = false, $encode = true)
Die Parameter werden in Tabelle 4.1 vorgestellt.
99
4 Die View erstellen Tabelle 4.1 Parameter für die View-Hilfsklasse url() Parameter
Beschreibung
$urlOptions
Array mit Optionen, mit denen der URL-String erstellt werden kann.
$name
Name der Route, die für die Erstellung des URL-Strings verwendet wird. Falls null, wird der Routenname verwendet, der ursprünglich auf den URL der aktuellen Seite passte.
$reset
Wird auf true gesetzt, um alle Parameter zurückzusetzen, wenn der URLString erstellt wird.
$encode
Wird auf true gesetzt, damit mit urlencode() alle Parameterwerte in $urlOptions kodiert werden
Bei Verwendung der Standardroute können Sie Action, Modul, Controller und andere erforderliche Parameter übergeben, und der korrekte URL wird generiert. Wenn Sie bei einigen dieser Felder keine Eingaben machen, werden die aktuell bekannten Einträge genommen. Wenn wir einen URL für die Browse-Action im place-Controller generieren wollten, dann sähe die Zeile so aus: url(array('controller'=>'place', 'action'=>'browse')); ?>
Wenn sich die aktuelle Seite allerdings im place-Controller befindet, können wir diesen Teil weglassen und nur Folgendes schreiben: url(array('action'=>'browse')); ?>
Um der Controller-Action andere Parameter zu übergeben wie beispielsweise die Seitennummer der Liste der Zielorte, die durchsucht werden soll, dann fügen wir das als zusätzliche Schlüssel im Array ein: url(array('controller'=>'place', 'action'=>'browse', 'page'=>'2')); ?>
Damit wird der URL /base-url/place/browse/page/2 generiert. Ein Nebeneffekt ist, dass wenn Sie nun Links mit url() generieren, bei allen erstellten URL-Strings /page/2 eingefügt wird – es sei denn, Sie überschreiben das. Das Überschreiben kann man auf zweierlei Weise durchführen. Zum einen können Sie den Parameter auf null setzen: url(array('controller'=>'place', 'action'=>'browse', 'page'=>null)); ?>
Alternativ können Sie den dritten Parameter der url()-View-Hilfsklasse nehmen, der $reset lautet. Mit diesem Parameter ist gewährleistet, dass zur Erstellung des URLs keiner der bekannten Parameter verwendet wird. Er wird wie folgt verwendet: url(array('controller'=>'place', 'action'=>'browse'), null, true); ?>
Wie Sie sehen können, ist url() sehr leistungsfähig und erlaubt eine flexible und einfach zu wartende URL-Erstellung innerhalb der View-Skripte.
100
4.4 Fortgeschrittene View-Hilfsklassen Mit dem nächsten Satz der verfügbaren View-Hilfsklassen wird es einfacher, View-Skripte auf separate Dateien aufzuteilen. Die View-Hilfsklassen partial() und partialLoop() helfen uns bei der Verwaltung der View-Skripte, und wir haben nicht zuviel Code in einer einzelnen Datei.
4.4.2
Die Verwaltung der View-Skripte
Es ist nicht unüblich, dass eine einzelne Antwort aus mehreren View-Skripten besteht. Wir haben dies bereits in Listing 4.3 gesehen, wo die View-Skripts für Kopf- und Fußzeile mit layout.phtml verlinkt sind. Es gibt zwei zusammenhängende View-Hilfsklassen für die View-Skript-Verwaltung: partial() und partialLoop(). 4.4.2.1
Die View-Hilfsklasse partial()
Mit partial() wird ein separates View-Skript innerhalb seines eigenen Geltungsbereichs gerendert. Das bedeutet, dass keine der Variablen, die der View zugewiesen wurden, im Ziel-View-Skript verfügbar sind, und nur die speziell übergebenen Variablen vorhanden sind. Wenn Sie ein separates View-Skript innerhalb des gleichen Kontexts wie das aktuelle View-Skript rendern, wird die render()-Methode des Zend_Views benutzt. Standardmäßig wird partial() wie folgt verwendet: partial("menu.phtml", array('title'=>'home', 'url'=>'/')); ?>
Damit wird das View-Skript menu.phtml gerendert, und die einzigen beiden Variablen, die darin verfügbar sind, sind $this->title und $this->url. Die an partial() übergebenen Parameter bezeichnet man als Model, und dabei kann es sich entweder um Arrays oder Objekte handeln. Wenn ein Objekt verwendet wird, werden dem View-Partial entweder die toArray()-Methode oder alle öffentlichen Variablen zugewiesen. Die View-Hilfsklasse partialLoop() erweitert partial(), wodurch das gleiche ViewSkript mehrmals aufgerufen werden kann. 4.4.2.2
Die View-Hilfsklasse partialLoop()
Ein üblicher Anwendungsfall von partiellen View-Skripts ist das Iterieren über eine Liste von Daten – so wie hier: menu as $menuItem) { echo $this->partial("menu.phtml", $menuItem); } ?>
Das kommt so häufig vor, dass man anhand der View-Hilfsklasse partialLoop() diesen Code wie folgt vereinfachen kann: partialLoop('menu.phtml', $this->menu) ?>
101
4 Die View erstellen Hinter den Kulissen ruft partialLoop() dann partial() auf, also funktioniert es auf exakt die gleiche Weise. Ein Zeilenobjekt in einer partiellen Schleife platzieren Wenn Sie (Standard-Einstellungen vorausgesetzt) partialLoop() mit einem Zeilensatz verwenden, dann werden Sie merken, dass das Zeilenobjekt in einen Satz von Elementvariablen umgewandelt wird, wenn es im View-Skript verwendet wird. Wenn Sie das Objekt selbst erstellen wollen, geht das wie folgt: partialLoop()->setObjectKey('menu')¬ >partialLoop('menu.phtml', $this->menu) ?>
Die Variablen sind nun im View-Skript durch die Verwendung von $this->menu-> {Variablenname} verfügbar. Die Nutzung von partial() und partialLoop() kann die Wiederverwendbarkeit von HTML-View-Skripts signifikant verbessern – auf die gleiche Weise, wie PHP-Code durch Klassen und Funktionen wiederverwendbar wird. Wir wenden uns nun der Verwaltung des Kopfabschnitts einer HTML-Seite mit den vielen head*()-View-Hilfsklassen zu.
4.4.3
Hilfsklassen für HTML-Kopfzeilen
Das Zend Framework enthält einen Satz von Hilfsklassen, um den -Abschnitt einer HTML-Seite zu verwalten. Mit diesen Hilfsklassen können Sie die Informationen vorab einrichten und im Layout-View-Skript ausgeben. Es gibt verschiedene Hilfsklassen, die meist mit dem Wort head anfangen und zwei (nämlich json() und doctype()), die ebenfalls in der Kopfzeile eines Dokuments verwendet werden. 4.4.3.1
Die Hilfsklasse json()
Mit json() kann man JSON-Daten an den Browser senden, meist als Antwort auf eine Ajax-Anfrage. Damit werden drei Dinge ausgeführt: Die Daten werden ins JSON-Format kodiert. Das Rendern des Layouts wird deaktiviert. Der HTTP-Header content-type wird auf application/json gesetzt. Die Verwendung ist sehr einfach. Ein gegebenes Array oder Objekt namens data wird kodiert und anhand des folgenden Codes an den Browser gesendet: json($this->data)); ?>
Diese View-Hilfsklasse arbeitet zur Kodierung der Daten mit der Komponente Zend_Json. Diese Komponente wird die json-PHP-Erweiterung nehmen, falls sie verfügbar ist; ansonsten greift sie auf eine native PHP-Implementierung zurück.
102
4.4 Fortgeschrittene View-Hilfsklassen Alle anderen View-Hilfsklassen für die HTML-Kopfzeile geben Text in die HTML-Seite aus. Fangen wir mit der View-Hilfsklasse doctype() an, weil diese auf der Seite ganz oben verwendet wird. 4.4.3.2
Die Hilfsklasse doctype()
Die Hilfsklasse doctype() ist zum Erstellen des Dokumenttyps in der HTML-Datei sehr praktisch. Sie löst vor allem das Problem, den korrekten Dokumenttyp auf der Website des W3C nachschauen zu müssen. Wie bei allen View-Hilfsklassen geben Sie sie einfach aus: doctype('XHTML1_TRANSITIONAL'); ?>
versteht alle doctype-Deklarationen für XHTML 1 und HTML 4, und das wirkt sich auch auf das Rendern der relevanten head*()-View-Hilfsklassen aus, damit sie zum angegebenen doctype konform sind. Sie können anhand der Elementfunktion isXhtml() auch das Vorhandensein von XHTML in anderen View-Skripts prüfen. doctype()
Tipp
Manche View-Hilfsklassen (vor allem die auf Formulare bezogenen) werden entweder HTML- oder XHTML-konformen Code ausgeben, was vom Wert abhängt, der der doctype()-View-Hilfsklasse übergeben wird. Aus diesem Grund ist es am besten, den Dokumenttyp im Dispatch-Prozess schon recht früh anzugeben, z. B. im dispatchLoopStartup() Ihres Front-Controller-Plug-ins ViewSetup. Sie müssen es immer noch oben in Ihrem XHTML-Output ausgeben, aber dann brauchen Sie den Wert nicht noch einmal zu übergeben.
4.4.3.3
Die Hilfsklasse headLink()
Mit headLink() werden die -Elemente im -Abschnitt des Dokuments verwaltet. Dazu gehören CSS-Stylesheets, Favicons, RSS-Feeds und Trackbacks. Damit werden die Elemente beim Rendern jedes View-Skripts gesammelt, und später wird es dazu verwendet, die Elemente im Layout zu rendern. Wir haben diese Funktionalität in Listing 4.9 verwendet, dem View-Skript menu.phtml, das für das Menü eine CSS-Datei angegeben hat: headLink()->appendStylesheet($this->baseUrl() . '/css/menu.css') ?>
Es gibt auch eine Elementfunktion prependStylesheet(), um die Reihenfolge des Outputs zu steuern, und damit man mit den alternativen Stylesheets appendAlternate() und prependAlternate() arbeiten kann. Über den zweiten Parameter können wir auch die Medienausgabe für das Stylesheet setzen. Für ein Print-Stylesheet würde man dann den folgenden Code verwenden: headLink()->appendStylesheet($this->baseUrl(). '/css/menu.print.css', 'print') ?>
Favicons werden auf ähnliche Weise gesetzt, nur dass wir die generische Elementfunktion headLink() verwenden anstatt der CSS-spezifischen Funktionen:
103
4 Die View erstellen headLink()->headLink(array('rel' => 'favicon', 'href' => $this->baseUrl().'/favicon.ico'))?>
All die angegebenen Links werden dann anhand dieser Zeile gerendert: headLink(); ?>
4.4.3.4
Die Hilfsklasse headScript()
Ähnlich wie headLink() wird die Hilfsklasse headScript() verwendet, um JavaScriptDateien einzubinden. Somit können wir die relevanten Dateien der Hilfsklasse hinzufügen, wenn die Views gerendert werden, und um dann den finalen Output später zu rendern. Das ist hinsichtlich der Wartung ganz wichtig, weil es bedeutet, dass die Referenzen auf das JavaScript sich innerhalb des korrekten Action-View-Skripts anstatt separat im Layout befinden. Das Hinzufügen von JavaScript-Dateien ist sehr ähnlich wie bei CSS-Dateien und wird wie folgt erledigt: $this->headScript()->appendScript($this->baseUrl().'/js/autocomplete.js');
Wenn alle Skripts hinzugefügt wurden, werden sie wie folgt über die Hilfsklasse gerendert: headScript(); ?>
Was ganz praktisch ist: headScript() kümmert sich für uns darum, dass wir nur eine Referenz auf eine externe JavaScript-Datei einbinden, auch wenn wir sie vielleicht mehr als einmal eingefügt haben. Wir müssen uns nicht selbst darum kümmern. Dies sorgt für minimale Seitenladezeiten und bedeutet damit eine gute Optimierung. 4.4.3.5
Die Hilfsklasse headMeta()
Wie man schon am Namen sieht, werden anhand von headMeta() alle <meta>-Tags im der Seite gesetzt. Es gibt zwei Arten von Meta-Tags: name und httpequiv, und somit gibt es auch zwei Sets mit Funktionen (siehe Tabelle 4.2).
-Abschnitt
Tabelle 4.2 Die View-Hilfsklassenfunktionen headMeta() name-Version
4.4 Fortgeschrittene View-Hilfsklassen Das Feld $keyValue setzt entweder den name- oder den http-equiv-Schlüssel für das Tag. Der Parameter $content wird für das Wertattribut eines name-Tags oder das Inhaltsattribut eines http-equiv-Tags verwendet, und der $modifiers-Parameter ist ein assoziatives Array, das bei Bedarf die Attribute lang und scheme enthalten kann. Schauen wir uns das Meta-Tag keywords an. Normalerweise würde es im Action-ViewSkript gesetzt, das aus dem Inhalt basiert, der von der Action gerendert wird. In Places setzen wir die Schlüsselwörter für jeden Zielort während der Index-Action des placeControllers, weil dies der Controller ist, der eine Seite mit einem einzigen Zielort darstellt. Die Schlüsselwörter werden in der Datenbank gespeichert und somit der View zusammen mit allen anderen Daten über den Zielort zugewiesen. Das Action-View-Skript rendert dann die Informationen wie Titel und Beschreibung für den Besucher und richtet außerdem das Meta-Tag keywords anhand des folgenden Codes ein: $this->headMeta()->appendName('keywords', $this->place->keywords);
Der Browser soll außerdem die Seite cachen, aber nicht zu lange, weil ja neue Rezensionen eingegeben werden. Darum werden wir auch das http-equiv-Metag-Tag expires anhand des Codes aus Listing 4.16 hinzufügen. Listing 4.16 Das Meta-Tag expires anhand von Zend_Date einrichten
Ergänzt aktuelle Uhrzeit/ $date = new Zend_Date(); Datum um 3 Stunden $date->add('3', Zend_Date::HOUR); $this->headMeta()->appendHttpEquiv('expires', Erfordert Datumsformat $date->get(Zend_Date::RFC_1123));
aus RFC 1123
Wir haben ein Ablaufdatum gewählt, das drei Stunden in der Zukunft liegt. Das kann man ganz einfach über die Funktion add() von Zend_Date erreichen n, weil es sehr leicht geht, einen beliebigen Zeitraum bei einem Objekt hinzuzufügen oder abzuziehen. Das Meta-Tag expires erfordert ein Datumsformat des im RFC 1123 definierten Typs, das direkt von der Funktion get() von Zend_Date unterstützt wird o. Wie wir in Listing 4.3 gesehen haben, gibt die Datei layout.phtml die Meta-Tags anhand dieser Zeile aus: headMeta(); ?>
Damit werden alle Meta-Tags, die im Verlaufe des Renderns der Seite hinzugefügt worden sind, korrekt formatiert für uns ausgegeben. 4.4.3.6
Die Hilfsklasse headTitle()
Mit dieser Hilfsklasse wird das -Tag innerhalb des Abschnitts gesetzt. Anhand dieser Hilfsklasse können Sie einen Titel mit mehreren Abschnitten erstellen, während die Dispatch-Schleife des Front-Controllers sie startet und darstellt. Das verwendet man normalerweise zur Darstellung des Seitentitels und des Namens der Website, z. B.: „London Zoo – Places to take the kids.”
105
4 Die View erstellen Bei Places wird es zum Front-Controller-Plug-in Places_Controller_Plugin_ViewSetup eingefügt, das in Listing 4.2 vorgestellt wurde. Um den Seitentitel einzufügen, rufen wir das View-Skript headTitle() direkt im Action-Controller auf. Für die Homepage müssen wir also folgenden Code in der indexAction() des IndexControllers einfügen: $this->view->headTitle('Welcome');
Wir müssen schlussendlich noch den Namen der Website einbauen. Das besorgt die postDispatch()-Funktion des Places_Controller_Plugin_ViewSetups, die nach jedem Dispatch gestartet wird. Diese Funktion wird in Listing 4.17 gezeigt. Listing 4.17 Einen Namen für die Website mit der Methode postDispatch() einfügen public function postDispatch(Zend_Controller_Request_Abstract $request) { if (!$request->isDispatched()) { Sucht nach return; weiteren Actions }
$view = $this->_view;
Weist $title des
if (count($view->headTitle()->getValue()) == 0) { $view->headTitle($view->title); }
Views zu, falls nichts gesetzt ist
$view->headTitle()->setSeparator(' - ');
Setzt
Separator
$view->headTitle('Places to take the kids!'); }
Setzt Site-Name
Wir wollen, dass nur der Name der Website in den Titel eingefügt wird, wenn das die letzte Action ist, die dispatcht wird. So können andere Actions bei Bedarf in den Titel zusätzliche Informationen einfügen. Das prüfen wir, indem wir uns die isDispatched()Methode der Anfrage anschauen und aus der Funktion zurückspringen, falls sie false ist n. Wir haben uns bereits nach der Konvention gerichtet, dass das Haupt-Tag
für die Seite mit dem Wert von $view->title gefüllt wird. Also prüfen wir der Einfachheit halber, ob die Hauptüberschrift gesetzt wurde, und falls nicht, dann nehmen wir den Titel der Seite o. Standardmäßig gibt es keinen Separator zwischen den Segmenten des Titels, also müssen wir einen setzen p. Bei Places haben wir uns für einen Trennstrich entschieden, obwohl man auch einen Doppelpunkt oder einen Schrägstrich nehmen könnte. Zum Schluss fügen wir den Namen der Website ein q. Wieder stellen wir den Titel in Listing 4.3 anhand dieses Codes dar: headTitle(); ?>
Wir haben uns nun all die häufig verwendeten head*()-View-Hilfsklassen angeschaut, mit denen man den -Abschnitt der Webseite besser verwalten und warten kann. Der wichtigste Vorteil bei der Arbeit mit den View-Hilfsklassen headLink() und headScript() ist, dass Sie das auf die Action bezogene CSS und JavaScript innerhalb des Action-
106
4.5 Zusammenfassung View-Skripts behalten können, während sie weiterhin an der korrekten Stelle im Dokument gerendert werden. Das Framework enthält noch eine ganze Reihe anderer View-Hilfsklassen, die hier nicht angesprochen wurden, weil sie zu Zend_Form und Zend_Translate gehören. Sie werden in den jeweiligen Kapiteln erläutert.
4.5
Zusammenfassung Wir schauten uns hier eine vollständige Applikation an, die die MVC-Komponenten des Zend Frameworks verwendet, um den Code zu separieren und zu gewährleisten, dass er einfach zu pflegen ist. Indem wir uns auf die View konzentriert haben, haben wir erfahren, welch leistungsfähiges Tool das Designpattern Composite-View ist und wie wir damit ein einheitliches Look & Feel bei der Site umsetzen können. Mit der Komponente Zend_Layout können Sie ein Layout-Skript ganz einfach ohne inhaltliche Platzhalter implementieren. Mit den fortgeschrittenen View-Hilfsklassen ist dafür gesorgt, dass das Layout so flexibel wie nötig ist und wir gleichzeitig die Möglichkeit haben, die anderen Controller-Actions direkt aufzurufen. Durch diese Trennung wird jeder Controller-Action nur eine Verantwortlichkeit übertragen, was dazu führt, dass sie leichter zu warten und zu entwickeln ist. In den Kapiteln 3 und 4 haben wir die Grundlage von Places to take the kids! geschaffen, um daraus eine voll funktionsfähige Website zu machen. Der begleitende Quellcode für dieses Buch zeigt den kompletten Code, der dafür erstellt wurde. Wir können uns nun also damit beschäftigen, wie das Zend Framework mit Ajax zusammenarbeitet.
107
5 5
Ajax
Die Themen dieses Kapitels
Ajax in Webapplikationen Ein einfaches Beispiel mit Ajax YUI in Ajax integrieren Ajax-Elemente in Zend Framework-Applikationen integrieren Mit Ajax können anhand von JavaScript interaktive Webseiten erstellt werden, die hinter den Kulissen Daten an den Server übermitteln und vom Server erhaltene Daten verarbeiten. Das bedeutet, dass der User keinen erneuten Seitenaufbau sieht und es ihm so vorkommt, dass die Website schneller reagiert. Als Folge davon wirkt es, dass sich eine Webapplikation (z. B. ein webbasierter E-Mail-Client) eher wie seine Desktop-Verwandtschaft verhält. Obwohl das MVC-System (das die verschiedenen Schichten der Applikation voneinander trennt) des Zend Frameworks serverbasiert ist, können Sie Ihren Websites damit einfacher Ajax-Funktionalitäten verleihen. In diesem Kapitel schauen wir uns an, was Ajax ist und wie es in Webapplikationen eingesetzt wird. Wir werden auch alle Komponenten eines einfachen Beispiels untersuchen, sowohl in reinem JavaScript als auch mit bereits vorab erstellten Ajax-Libraries. Wir werden Ajax außerdem in eine Zend Framework-Applikation integrieren, damit wir untersuchen können, wie Ajax mit dem MVC-System interagiert. Zuerst geht es jedoch darum, was Ajax eigentlich genau ist.
5.1
Kurze Einführung in Ajax Wie bereits angemerkt sind mit Ajax arbeitende Applikationen benutzerfreundlicher, weil sie schneller reagieren. Abbildung 5.1 zeigt Google Suggest (http://www.google.com/ webhp?complete=1&hl=en). Bei dieser Ajax-Applikation klappt eine Dropdown-Liste mit
109
5 Ajax sortierten Suchvorschlägen auf, wenn Sie Ihre Anfrage in das Suchfeld eingeben. Ein weiteres Beispiel einer guten Ajax-Applikation ist Google Calendar (http://calendar.google. com). Hier können Sie Kalendereinträge per Drag & Drop verschieben, um das Datum bzw. dessen Uhrzeit zu verändern. Der Einsatz von Ajax weist Vor- und Nachteile auf. Die Hauptvorteile bestehen darin, dass die Benutzerschnittstelle intuitiver ist, der Workflow für den User deutlich weniger unterbrochen wird, dass die Applikation reaktionsfreudiger wirkt und weniger Bandbreite benötigt wird, weil nur die erforderlichen Daten zum Server geschickt und abgeholt werden. Der große Nachteil ist, dass bei Ajax-Applikationen wohlbekannte Browser-Features wie die Zurück-Schaltfläche und verlässliche URLs in der Adressleiste für Lesezeichen oder das Schreiben von E-Mails nicht mehr funktionieren. Webdesigner müssen sich noch um weitere Probleme der Benutzeroberfläche kümmern, z. B. wie man darstellt, dass etwas gerade bearbeitet wird, weil der „drehende Kreis“ des Internet Explorers (IE) seiner Aufgabe nicht mehr nachkommt. Für Websites, die mit Ajax arbeiten, könnte es auch schwierig sein, die Accessibility-Richtlinien der Web Accessibility Initiative (WAI) zu erfüllen. Somit ist oftmals ein System erforderlich, auf das im Notfall zurückgegriffen werden kann.
5.1.1
Definition von Ajax
Ajax ist seit 2005 ein feststehender Begriff: Damit wird eine Suite von Technologien beschrieben, mit denen man dynamische Websites erstellen kann. Das Akronym bedeutet ausgeschrieben Asynchronous JavaScript and XML (also asynchrones JavaScript und XML). Gehen wir der Reihe nach die Technologie jeder Komponente durch.
Abbildung 5.1 Google Suggest wurde bei Googles „Labs“ entwickelt und nutzt Ajax, um eine kontextabhängige Autovervollständigung zu ermöglichen.
5.1.1.1
Asynchron
Damit eine Ajax-Applikation ihrem Namen auch gerecht wird, muss sie mit dem Server sprechen können, ohne dass ein erneuter Seitenaufbau (page refresh) nötig wird. Das bezeichnet man als asynchronen Datenaustausch, der allgemein anhand des XMLHttpRequest-
110
5.1 Kurze Einführung in Ajax Objekts des Browsers durchgeführt wird. Man kann auch mit einem versteckten iframeElement arbeiten. Das XMLHttpRequest-Objekt ist im Wesentlichen ein in den Browser eingebauter Minibrowser, über den wir mit dem Webserver sprechen, ohne den User zu stören. 5.1.1.2
JavaScript
Die zentrale Komponente bei den mit Ajax arbeitenden Technologien ist JavaScript und das DOM (Document Object Model) des Browsers, durch die die Webseite anhand von Skripten manipuliert werden kann. JavaScript ist für sich genommen eine vollständige Programmiersprache und wird seit 1995 von allen wichtigen Browsern implementiert. Das DOM ist ein standardisierter Weg, um die Elemente einer Webseite als Baum von Objekten zu repräsentieren, die mit JavaScript manipuliert werden können. Innerhalb von AjaxApplikationen ist das der Mechanismus, mit dem die Applikation die Webseite dynamisch verändern kann, damit neue Informationen gezeigt werden können, ohne dass der Server neue Daten senden muss. 5.1.1.3
XML
Um die neuen Daten vom Server zum Browser in einer asynchronen Anfrage zu übertragen, werden die Daten mit XML formatiert. Generell unterscheidet sich die Sprache, mit der die Server-Applikation geschrieben wird, von dem im Browser verwendeten JavaScript. Also wird ein sprachneutrales Format für den Datentransfer verwendet. XML ist ein allgemein bekannter Standard, um diese Art von Problem zu lösen, doch in AjaxApplikationen werden auch andere Formate eingesetzt: Strukturiertes HTML, Text und JSON (ein auf JavaScript basierendes Datentransferformat) sind hier zu nennen. 5.1.1.4
Nicht nur XML
Anstatt von XML kann auch JSON verwendet werden; die Applikation wird dann immer noch als Ajax-Applikation bezeichnet (weil Ajax einfach besser klingt als „Ajaj“!). Der Begriff Ajax hat seine Bedeutung über die ursprüngliche Definition hinaus erweitert, und heutzutage bezeichnet er eine Klasse von Technologien, die eine dynamische UserInteraktion mit dem Webserver ermöglicht. Es gibt viele Einsatzmöglichkeiten, wie Ajax auf einer Website eine bessere User Experience ermöglicht. Schauen wir uns kurz an, wie Ajax im Internet eingesetzt wird, um Usern das Leben zu erleichtern.
5.1.2
Ajax in Webapplikationen
Ajax kann in allen möglichen Webapplikationen auf vielfältige Weise verwendet werden. Zu den Haupteinsatzgebieten gehören: Übermittlung von Daten ohne einen erneuten kompletten Seitenaufbau Validierung von Formularen
111
5 Ajax
Hilfsfunktionen in Formularen (Autovervollständigung und Dropdown-Listen) Bewegen von Elementen auf einer Seite per Drag & Drop Schauen wir uns diese Einsatzmöglichkeiten genauer an. 5.1.2.1
Übermittlung von Daten ohne neuerlichen kompletten Seitenaufbau
Webapplikationen wie Gmail und der Kalender von Google arbeiten vollständig mit Ajax. Darüber werden die Daten direkt vom Server ausgelesen, ohne dass eine Auffrischung der Seite erforderlich ist. So können Daten im relevanten Teil der Seite aktualisiert werden, wenn man beispielsweise auf eine E-Mail im Posteingang klickt, ohne dass der ganze Bildschirm weiß wird, während sich die Seite neu aufbaut. Webapplikationen reagieren außerdem schneller, wenn sie mit Ajax arbeiten, weil bei jeder Benutzereingabe weniger Daten übermittelt werden müssen. Weil die Seite nicht immer wieder neu aufgebaut werden muss, können die User in die Arbeit mit der Applikation auch regelrecht „eintauchen“, weil ihr Workflow nicht gestört wird. 5.1.2.2
Die Validierung von Formularen
Seitdem Netscape die Sprache erfunden hat, wurden mit JavaScript schon Formulare validiert. Normalerweise ist die Validierung ein Stück Ad-hoc-Code oberhalb des Formulars, der nach häufig vorkommenden bzw. offensichtlichen Fehlern sucht. Die eigentliche Validierung bleibt dem Server überlassen. Weil Formulare nicht der einzige Weg sind, um Daten an eine bestimmte Webseite zu senden, bleibt die serverseitige Validierung unverzichtbar. Doch je mehr von der Validierung abgewickelt werden kann, bevor der User darauf warten muss, dass die Seite vollständig aufgefrischt wird, desto besser ist es! Nichts ist frustrierender, als auf den Übermitteln-Button zu klicken, dann zehn Sekunden zu warten, nur um dann darüber informiert zu werden, dass die Telefonnummer nicht das korrekte Format hat! 5.1.2.3
Hilfsfunktionen in Formularen: Autovervollständigung und Dropdown-Listen
Formulare sind im Allgemeinen kompliziert, und alles ist willkommen, was dem Benutzer beim Ausfüllen hilft. Die Autovervollständigung von Formularfeldern ist so hilfreich, dass alle wichtigen Browser dieses Feature anbieten und sich merken, was Sie in ein bestimmtes Feld eingegeben haben. Ajax-Applikationen gehen bei dieser Idee noch einen Schritt weiter und helfen dem User dabei, Textfelder immer korrekt auszufüllen. DropdownListen in Formularen, die zu viele verschiedene Optionen anbieten, können durch Textfelder ersetzt werden, bei denen die Autovervollständigung aktiviert ist. Ein Beispiel dafür wären Felder, bei denen Sie Bundesland oder Heimatort auswählen können, wenn Sie Ihre Adresse eingeben. Die Autovervollständigung von Formularfeldern ist auch sehr praktisch, wenn der User entweder einen neuen Wert eingeben oder einen vorhandenen verwenden soll. Ein Beispiel dafür wäre, eine Aufgabe (Task) in einer Applikation für Projektmanagement einer Kate-
112
5.2 Ein einfache Beispiel für Ajax gorie zuzuweisen. Meistens soll der User aus einer vorhandenen Kategorie auswählen (und sie nicht falsch eintippen!), und dafür ist die Dropdown-Liste mit einer Autovervollständigung eine gute Hilfe. Wenn jedoch mal eine neue Kategorie erstellt werden soll, wird der Workflow nicht unterbrochen, weil die neue Kategorie direkt in das Feld eingetippt werden kann. 5.1.2.4
Drag & Drop
In der Arbeit mit Desktop-Computern kommt Drag & Drop sehr häufig vor. Sie wählen beispielsweise in einem Dateimanager einige Dateien aus und ziehen Sie in einen anderen Ordner. Mit Ajax kann diese Metapher für einen Arbeitsvorgang auch ins Internet übertragen werden. Sie können z. B. Waren aus einem Warenkorb in einem Online-Shop in einen Papierkorb ziehen, um sie aus der Bestellung zu entfernen. TIPP
Beim Drag & Drop im Internet ist zu beachten, dass die meisten Webapplikationen dies nicht bieten. Somit erwarten User nicht, dass es möglich ist. Sie sollten somit immer auch eine alternative Methode zur Durchführung der Aktion anbieten oder sehr klare Anweisungen schreiben!
Nachdem wir uns nun grob mit Ajax vertraut gemacht haben, wenden wir uns einer einfachen Beispielapplikation zu, die eine Anfrage per JavaScript an den Server zurückschicken und dessen Antwort verarbeiten kann.
5.2
Ein einfache Beispiel für Ajax In dieser Beispielapplikation verwenden wir die Formularvalidierung und prüfen, ob ein bestimmter Username akzeptabel ist. Als Mindestanforderung für ein ganz einfaches Beispiel brauchen wir drei Dateien:
eine HTML-Seite für das Formular eine JavaScript-Datei, um den XMLHTTPRequest auszuführen eine PHP-Datei, die sich um die serverseitige Validierung kümmert. Wenn der User die Zeichen in das Formularfeld eingibt, erscheint darunter eine Nachricht, die ihn über etwaige Fehler in der Namenswahl informiert. Abbildung 5.2 zeigt eine solche Applikation im Einsatz.
Abbildung 5.2 Ein einfaches Ajax-Beispiel zeigt eine Fehlermeldung, während der User etwas ins Textfeld tippt.
113
5 Ajax Der Fluss der Informationen in einer Ajax-Anfrage ist etwas komplizierter als bei einer einfachen Anfrage für eine Webseite. Alles fängt damit an, dass ein HTML-Element eine JavaScript-Callback-Funktion (z. B. onclick) enthält, die den JavaScript-Callback-Code ausführt. Der JavaScript-Callback initiiert über das XMLHttpRequest-System eine Anfrage an den Webserver, und der serverseitige Code (z. B. PHP) führt die Aufgabe durch. Sobald der PHP-Code fertig ist, formatiert er die Antwort entweder in XML oder JSON und schickt sie zurück an den Browser, wo sie von einem JavaScript-Callback weiterverarbeitet wird. Schließlich aktualisiert das JavaScript die HTML-Darstellung, um den Text zu ändern, anhand des DOMs neue Elemente hinzuzufügen oder CSS-Stile zu ändern. Dieser Prozess wird in Abbildung 5.3 gezeigt. Browser HTML JavaScript„on“-Event
DOM-Elemente aktualisieren JavaScript-Code
XMLHttpRequestAufruf an den Server
Antwortdaten in XML oder JSON
PHP-Verarbeitungscode Browser
Abbildung 5.3 Der Applikationsdatenfluss in einer AjaxAnfrage wird vom JavaScript-Code gesteuert. Der User macht etwas, was eine Anfrage auslöst und die Webseite aktualisiert, wenn eine Antwort empfangen wird.
Bei diesem Beispiel beginnen wir mit dem serverseitigen PHP-Validierungscode aus Listing 5.1, der prüft, ob der eingegebene Username mindestens vier Zeichen aufweist und nicht schon vorhanden ist. Listing 5.1 Eine einfache serverseitige Validierungsroutine in PHP: ajax.php lang genug ist Username is less than 4 characters '; } elseif (in_array($username, $existingUsers)) { Prüft, ob return '<span class="error"> Username Username already exists bereits '; vorhanden ist } else { return '<span class="ok"> Username is acceptable '; } } $name = isset($_GET['name']) ? $_GET['name'] : ''; echo checkUsername(trim($name));
114
5.2 Ein einfache Beispiel für Ajax Beachten Sie, dass wir den eigentlichen Validierungscode in eine Funktion namens checkUsername() gesteckt haben, damit man einfacher testen kann. Das Ergebnis von checkUsername() wird auf der Konsole ausgegeben, damit der Browser es für den User darstellen kann. In diesem Beispiel ist die Liste der vorhandenen User zwar ein Array ist, doch es ist wahrscheinlicher, dass das Skript einen Datenbankaufruf durchführen wird, um die aktuelle Userliste zu prüfen. Um auf das Validierungsskript zugreifen zu können, brauchen wir ein Formular, in das der User seinen Wunschnamen eingeben kann. Das HTML dafür steht in Listing 5.2. Listing 5.2 HTML-Datei mit einem einfachen Formular, das validiert werden muss: index.html Simple Ajax Example <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <style> Einfaches Styling .ok {color: green;} .error {color: red;} <script type="text/javascript" src="ajax.js" />
Registration:
zurückgegebene Nachricht
Dies ist ein einfaches Formular, das anhand des onkeyup-Events im Eingabefeld den aktuell vom User eingegebenen Text an die PHP-Datei auf dem Server schickt. Das wird in der JavaScript-Funktion check() erledigt, die in einer separaten JavaScript-Datei namens ajax.js gespeichert ist (siehe Listing 5.3). Listing 5.3 Selbst geschriebener JavaScript-Code für eine Validierungsanfrage an den Server var request; if (navigator.appName == "Microsoft Internet Explorer") { request = new ActiveXObject("Microsoft.XMLHTTP"); } else { request = new XMLHttpRequest(); }
Wird ausgeführt, wenn Anfrageobjekt seinen Zustand ändert
Stellt bei Erfolg zurückgegebenen Text dar
}
Um die HTML-Seite mit den neuen Daten zu aktualisieren, wählen wir über docuein Element auf der Seite aus und ändern dessen Attribute. In diesem Fall ändern wir es komplett, indem wir dessen Eigenschaft innerHTML durch die Antwort des Servers ersetzen. Wir könnten genauso auch die CSS-Vorlage ändern oder neue untergeordnete HTML-Elemente erstellen (z. B. neue li-Elemente), die auf den vom Server empfangenen Daten basieren. ment.getElementById()
Nun sollten Sie eine ungefähre Vorstellung davon haben, was Ajax ist und wie die verschiedenen Komponenten zusammenwirken. Dies ist natürlich ein sehr einfach gestricktes Beispiel, weil es in diesem Buch nicht in erster Linie um Ajax geht. Um Ajax wirklich zu verstehen und einen Überblick über all die Möglichkeiten zu bekommen, empfehlen wir Ajax in Action von Dave Crane und Eric Pascarello (Addison-Wesley 2008). Der hier präsentierte JavaScript-Code sieht schön einfach aus und verletzt praktisch jede Regel der Defensiven Programmierung – es gibt überhaupt keine Fehlerprüfung! Neben der Tatsache, dass das Beispiel dadurch überfrachtet wäre, ist es außerdem schwer, eine korrekt arbeitende Fehlerprüfung für alle verfügbaren Browser hinzukriegen. Wir könnten allerdings alle Komplikationen in eine Client-Library abstrahieren, und zum Glück haben andere Leute das für uns schon erledigt. Wir werden als Nächstes Client-Libraries für Ajax untersuchen und schauen, wie damit der erforderliche Code vereinfacht werden kann.
5.3
Die Arbeit mit Client-Libraries für Ajax Früher war es sehr umständlich, das für eine Ajax-Applikation erforderliche JavaScript einzubauen. Die Funktion checkUsername() in Listing 5.1 mischt beispielsweise Code, der sich um die Anfrage kümmert, mit Code, bei dem es um die Erstellung des HTMLDokuments geht. Das ist in Bezug auf eine langfristige Wartbarkeit nicht gut, wie wir in Kapitel 2 gesehen haben, wo wir dem MVC-Designpattern entsprechend bei unserer Hauptapplikation Business-, Steuerungs- und Darstellungscode voneinander getrennt haben. Wir sollten auch beim clientseitigen Ajax-Code die Verantwortlichkeiten voneinander getrennt halten. Aber anstatt den gesamten Code selbst zu schreiben, können wir uns dazu einer Ajax-Library bedienen.
116
5.3 Die Arbeit mit Client-Libraries für Ajax Eine JavaScript-Library versetzt uns in die Lage, auf die Arbeiten anderer aufzubauen. Als Entwickler von PHP-Applikationen sind wir oft mehr daran interessiert, die Probleme unserer Kunden zu lösen, als uns mit der zugrunde liegenden Technologie herumzuschlagen. Mit einer JavaScript-Library können wir mit Ajax auf weitgehend ähnliche Weise, wie uns das Zend Framework bei der serverseitigen Applikationsentwicklung unterstützt, auf einer höheren Stufe arbeiten. Man kann unter vielen JavaScript-Libraries auswählen, und es ist nicht offensichtlich, woran man sich bei der Auswahl orientieren sollte. Ähnlich wie bei unserer Entscheidung für das Zend Framework berücksichtigen wir bei den zentralen Überlegungen für die Wahl einer JavaScript-Library Features, Performance, Dokumentation und Community auf. Mit dem Yahoo! User Interface (YUI) haben wir neben den zentralen zugrunde liegenden Klassen eine Gruppe von UI-Widgets, um die Erstellung von Ajax-Applikationen zu vereinfachen. Das Schöne an YUI ist, dass es von einem großen Unternehmen unterstützt wird und dass es ausgezeichnet dokumentiert ist und viele Beispiele hat. Außerdem gibt es eine Community, die mit der Library arbeitet und in der Extensions produziert werden, die die Standardkomponenten erweitern. Konvertieren wir unser Beispiel aus Abschnitt 5.2, damit wir YUI eingesetzt wird. Dafür müssen wir die erforderlichen YUI-Library-Dateien (yahoo.js und connection.js) in die HTML-Datei einbauen und dann den Code aus ajax.js wie in Listing 5.4 gezeigt verändern. Listing 5.4 Die Integration von YUI in die Ajax-Beispielanwendung: ajax.js var handleSuccess = function(o) { Stellt Ergebnisse bei Erfolg dar if(o.responseText !== undefined){ document.getElementById('message').innerHTML = o.responseText; } Leert Message bei Fehlschlag } var handleFailure = function(o) { document.getElementById('message').innerHTML = ""; }
function check(name) { var sUrl = "ajax.php?name=" + name; var callback = { success: handleSuccess, failure: handleFailure }; var request = YAHOO.util.Connect.asyncRequest('GET', sUrl, callback); }
Führt mit dem Connect-Objekt von YUI die Ajax-Anfrage aus
Wie Sie sehen, ist der Code ziemlich gleich. In diesem Fall wird die Klasse, die den XMLHttpRequest umhüllt, mit YAHOO.util.Connect bezeichnet, und wir rufen die statische Methode asyncRequest auf, um die Verbindung zum Server zu initiieren. Wieder wird ein Konfigurationsobjekt verwendet, um zu definieren, welche Callback-Funktionen
117
5 Ajax wir verwenden wollen, obwohl wir dieses Mal zuerst das Objekt callback erstellt und es dann der Funktion zugewiesen haben. Die Verwendung von YUI erleichtert die Entwicklung von Ajax-Applikationen deutlich und macht sie weitaus weniger fehleranfällig, als wenn sie von Grund auf neu geschrieben wird. Natürlich gibt es noch eine Menge anderer Client-Libraries wie beispielsweise jQuery, MooTools, Prototype und Dojo, die Sie sich auch mal anschauen sollten, bevor Sie sich entscheiden. Im restlichen Buch werden wir weiterhin mit der YUI-Library arbeiten – und zwar aus keinem geringeren Grund, als dass wir sie ziemlich gut finden! Nachdem wir uns die Entwicklung durch eine Library erleichtert haben, können wir uns anschauen, wie Ajax ins Zend Framework passt.
5.4
Ajax im Zend Framework Man muss bei der Verwendung von Ajax im Zend Framework daran denken, dass beim Zend Framework Version 1.5 keine Ajax-Komponente enthalten ist, aber dass sie mehrere Plug-ins für Hilfsfunktionen enthält. Überdies erleichtern das MVC-System und andere Komponenten wie Zend_Json den Einbau von Ajax-Features in die eigene Applikation Ajax-Features. Im ganzen Buch ist die Wartbarkeit der Applikation eine zentrale Designüberlegung, und die Trennung von Model und Controller von der View im Zend Framework erleichtert es, die standardmäßig auf HTML basierende View der meisten Seiten durch eine andere View zu ersetzen, insbesondere für Ajax-Anfragen. Anmerkung
Bei der Version 1.6 des Zend Framework ist die JavaScript-Library Dojo integriert, diese war zum Zeitpunkt des Schreibens aber noch nicht verfügbar. Der Dojo-Support wird als zusätzliche Option angeboten, und alle anderen JavaScript-Libraries wie YUI, jQuery und Prototype können mit dem Zend Framework 1.6 verwendet werden. In der Online-Anleitung stehen alle Einzelheiten, wie man die neuen Dojo-Komponenten verwendet.
Die Integration von Ajax in das Zend Framework nimmt zweierlei Gestalt an: die Verarbeitung der Ajax-Anfrage des Users innerhalb des Controllers und die Bereitstellung der JavaScript-Elemente, um die Antwort des Servers an den Client zu verarbeiten. Aus Sicht des Zend Frameworks sieht eine Ajax-Anfrage weitgehend so aus wie jede andere Anfrage, die die Applikation erreicht. Der wichtigste Unterschied besteht darin, dass die an den Client zurückgegebene Antwort ein Snippet aus HTML, XML oder JSON ist. Bei der Verarbeitung einer Ajax-Anfrage muss darauf geachtet werden, dass eine andere View verwendet und das Layout abgeschaltet wird, falls es eines gibt. Das erledigt die integrierte Action-Hilfsklasse AjaxContext, die im Framework enthalten ist. Wenn AjaxContext einen XMLHttpRequest entdeckt, wird basierend auf dem gewünschten Format ein alternativer View-Suffix gesetzt, die Layouts (falls vorhanden) werden deaktiviert und die korrekten Antwort-Header für das Format gesendet, falls es sich nicht
118
5.4 Ajax im Zend Framework um HTML handelt. Der Vorteil des MVC-Designpatterns wird spürbar, weil der Controller und das Model ohne Veränderungen wiederverwendet werden können, wenn man Ajax in eine Applikation einbaut. Einzig die View ist der Teil, der geändert werden muss, weil ja statt einer schön formatierten Webseite nur die Daten zurückgesendet werden. Schauen wir uns zuerst den Controller an.
5.4.1
Der Controller
Wenn Controller-Actions verfügbar gemacht werden, die auf Ajax-Aufrufe reagieren, darf die View keine vollständigen HTML-Seiten zurücksenden, sondern muss stattdessen HTML-Fragmente oder JSON- bzw. XML-Daten senden. Der Controller für die einfache Beispielapplikation aus Abschnitt 5.3 erfordert zwei Actions: eine zur Darstellung der Seite und eine als Reaktion auf die Ajax-Anfrage. Die Action für die Darstellung der Seite (siehe Listing 5.5) sollte Ihnen vertraut vorkommen. Listing 5.5 IndexController-Action zur Darstellung der Seite public function indexAction() { $this->view->baseUrl = $this->getRequest()->getBaseUrl(); }
Speichert Basis-URL zur View
Die indexAction()-Methode ist sehr einfach, weil wir darin nur den Wert für die baseUrl in der View speichern müssen. Die brauchen wir, damit wir den vollständigen Pfad für die JavaScript-Dateien referenzieren können. Als Reaktion auf den Ajax-Aufruf nehmen wir eine separate Action, die wir checkAction() nennen werden. Damit checkAction() auf einen Ajax-Aufruf reagieren kann, müssen wir auch den AjaxContext in init() einrichten (siehe Listing 5.6). Listing 5.6 IndexController::init() richtet den AjaxContext ein. public function init() { $ajaxContext = $this->_helper->getHelper('AjaxContext'); $ajaxContext->addActionContext('check', 'html'); $ajaxContext->initContext(); }
Erstellt Antworten im HTMLFormat
Initialisiert Kontextobjekt
Die Action-Hilfsklasse AjaxContext ist sehr einfach zu verwenden. Wir rufen für jede Controller-Action, die auf einen Ajax-Aufruf reagieren soll, addActionContext() auf. Wir müssen auch den Typ der Antwort definieren, den die Action produzieren wird – in diesem Fall ist es HTML. Schließlich initialisieren wir die Action-Hilfsklasse n, um deren preDispatch()-Hook zu registrieren, der das Rendern des Layouts abschalten und (basierend auf dem Format der eingehen Ajax-Anfrage) eine andere View-Skriptdatei wählen wird.
119
5 Ajax Die eigentliche serverseitige PHP-Arbeit, die als Reaktion auf die Ajax-Anfrage erledigt wird, haben wir schon fertig. Wir werden die gleiche ajax.php-Datei wiederverwenden, die wir auch bei den anderen Beispielen in diesem Kapitel genutzt haben, weil sie die ausgezeichnete Funktion checkUsername() enthält, die die eigentliche Validierung durchführt. Um die MVC-Separation aufrechtzuerhalten, legen wir diese Datei im Verzeichnis models ab und binden sie ein, damit sie in der Action genutzt werden kann: public function checkAction() { include ('models/ajax.php'); $name = trim($this->getRequest()->getParam('name')); $this->_view->result = checkUsername($name); }
Die verknüpfte View-Datei views/scripts/check.ajax.phtml enthält nur eine Zeile Code, um das Ergebnis auszugeben: result; ?>
Die View-Skriptdatei hat einen anderen Dateinamen, weil sie eine Ajax-Antwort ist. Somit hat die Action-Hilfsklasse AjaxContext den Dateinamen geändert und „ajax“ aufgenommen, um sie von der ansonsten verwendeten Standard-View-Skriptdatei zu unterscheiden. Andere Dateinamenendungen sind .json.phtml für JSON-Antworten und .xml.phtml für die mit XML. Das ist wahrscheinlich die einfachste View-Datei, die Sie je zu Gesicht bekommen! Wenden wir uns nun der Ajax-Seite zu.
5.4.2
Die View
Im Zend Framework ist der JavaScript-Teil einer Ajax-Anfrage im View-Abschnitt enthalten. Also verwaltet das View-Skript für die index-Action die JavaScript-Seite. Wie zu erwarten war, enthält die View (application/views/scripts/index/index.phtml) den HTML-Code für die Seite. Das ist der Gleiche wie vorher, außer dass wir angeben müssen, wo die JavaScript-Dateien zu finden sind. Dafür nehmen wir die in indexAction() erstellte Eigenschaft baseUrl: <script type="text/javascript" src="baseUrl; ?>/js/yahoo.js"> <script type="text/javascript" src="baseUrl; ?>/js/connection.js"> <script type="text/javascript" src="baseUrl; ?>/js/ajax.js">
Wir haben uns entschieden, bei der Ajax-Anfrage mit YUI weiterzumachen, doch Prototype würde genauso gut funktionieren. Der Rest der Datei index.phtml ist der gleiche wie der Code in Listing 5.2, der bereits gezeigt wurde. Die JavaScript-Funktion check() muss auch überarbeitet werden, weil wir einen vollqualifizierten URL für die Anfrage an den Server brauchen. Von daher übergeben wir die Basis-URL-Eigenschaft an check() und überarbeiten die JavaScript-Variable sUrl, sodass sie das Zend Framework-Format (controller/action/param_name/param_value) annimmt – siehe Listing 5.7.
120
5.5 Integration in eine Zend Framework-Applikation Listing 5.7 YUI-Ajax-Anfrage an die Controller-Action function check(baseUrl, name) { Erstellt URL var sUrl = baseUrl + "/index/ajax/format/html/name/" + name; var callback = { success: handleSuccess, Setzt failure: handleFailure, X_Requested_WITH}; Header YAHOO.util.Connect.initHeader('X_REQUESTED_WITH', 'XMLHttpRequest'); var request = YAHOO.util.Connect.asyncRequest('GET', sUrl, callback); }
Neben Controller und Action müssen wir noch der AjaxContext-Hilfsklasse sagen, in welchem Format die Resultate zurückgegeben werden sollen. In diesem Falle brauchen wir HTML, also ergänzen wir den zusätzlichen Anfrageparameter format mit dem Wert html in der Variable sUrl n. Wir müssen AjaxContext auch informieren, dass es sich bei der Anfrage um einen XMLHttpRequest handelt. Das erledigt der HTTP-Header X_REQUESTED _WITH. Beim Connect-Objekt des YUI nehmen wir die Methode initHeader()o. Wir haben nun ein funktionierendes Ajax-Beispiel, das die MVC-Struktur des Zend Frameworks verwendet. Die einzigen Änderungen, die an der einfachen Beispielapplikation vorgenommen wurden, dienten der Absicherung, dass der Code entsprechend seiner Rolle korrekt separiert wurde. Wir können uns jetzt etwas Interessanterem zuwenden: Wie wird Ajax vollständig in einer typischen Zend Framework-Applikation wie der Places-Website integriert?
5.5
Integration in eine Zend Framework-Applikation Wenn es um den Einsatz von Ajax in einer Zend Framework-Applikation geht, muss nicht nur das clientseitige JavaScript geschrieben werden. Wir müssen auch darauf achten, dass die View nicht versucht, als Reaktion auf eine Ajax-Anfrage eine komplette HTML-Seite dazustellen, und dass der Controller nicht die Arbeit erledigt, die für das Model gedacht war. Um diese Probleme im Kontext zu betrachten, werden wir ein Feedbacksystem in unser Places-Beispiel einbauen. Schauen wir uns folgenden Anwendungsfall an: Wir hoffen, dass Places sehr beliebt wird und wir eine Menge Rezensionen (reviews) für jedes Ausflugsziel bekommen. Wir möchten, dass die Nutzer uns unkompliziert darüber informieren können, ob eine bestimmte Rezension für sie hilfreich war. Wir können dann andere User darauf hinweisen, als wie hilfreich jede aufgelistete Rezension eingeschätzt wurde, wodurch sie auf der Site eine weitere Beurteilungsmöglichkeit bekommen. Wenn dann die Anzahl der Rezensionen zunimmt, können wir nur diejenigen auf der Hauptseite des Zielortes ausgeben, die am hilfreichsten sind, und die weniger hilfreichen auf eine sekundäre Seite verbannen.
121
5 Ajax Das ist eine ideale Aufgabe für Ajax, weil wir die User nicht mit dem Neuladen der Seite belästigen wollen: Eine Aktualisierung innerhalb der Seite erlaubt ihnen, sich die anderen Rezensionen weiter anzuschauen, wenn sie angegeben haben, ob eine bestimmte Rezension hilfreich ist oder nicht. Um für eine Rezension ein Feedback zu geben, muss die Frage „War diese Rezension für Sie hilfreich?“ gestellt werden, und die möglichen Antworten lauten Ja oder Nein. Wir halten nach, wie viele Leute mit Ja geantwortet haben, und zählen die Gesamtsumme der Antworten. Somit können wir den User dann darüber informieren dass „N von M Personen diese Rezension hilfreich fanden.“ In einer späteren Phase, wenn wir dann immer mehr Rezensionen bekommen haben, könnten wir uns sogar überlegen, die Besprechungen danach zu ordnen, wie hilfreich sie waren. Die Benutzerschnittstelle für das Feedbacksystem ist sehr einfach (siehe Abbildung 5.4).
Abbildung 5.4 Im Feedbacksystem für die Rezensionen kann man über zwei Schaltflächen seine Meinung kundtun.
Das Feedbacksystem besteht aus einer einzigen Textzeile, die die Anzahl der Personen ausgibt, die diese Rezension hilfreich fanden, und die Gesamtzahl der Antworten. Dem User bleibt dann überlassen zu bestimmen, ob dieses Verhältnis bedeutet, dass die Rezension „hilfreich“ ist. Wir haben auch zwei Schaltflächen, über die die User ihr Feedback geben. Wir erstellen dieses Feature, indem wir mit den View-Skripts anfangen und anschließend erst HTML und dann JavaScript einbauen. Wir brauchen auch zwei Datenbankfelder, die die beiden Zahlenwerte enthalten: helpful_yes für den ersten und helpful_total für den zweiten. Schauen wir uns den für dieses System notwendigen Code an und beginnen mit dem Controller.
5.5.1
Der Place-Controller
Das Wichtigste zuerst: Wir brauchen eine Controller-Action, um die Details eines Zielortes und die damit verknüpften Rezensionen darzustellen. Das ist recht unkompliziert. Wir brauchen nur eine Action in einem Controller. Weil diese Seite einen bestimmten Zielort darstellt, werden wir den Controller Place nennen und mit der index-Action arbeiten. Das bedeutet, dass wir es mit URLs der Form http://www.placestotakethekids.com/place/index/ id/{id} zu tun haben, die einfach zu verstehen sind. Listing 5.8 zeigt diesen Controller (ohne Fehlerprüfung!).
122
5.5 Integration in eine Zend Framework-Applikation Listing 5.8 Der Place-Controller: application/controllers/PlaceController.php class PlaceController extends Zend_Controller_Action { public function indexAction() { $placesFinder = new Places(); $id = (int)$this->getRequest()->getParam('id'); $place = $placesFinder->fetchRow('id='.$id); $this->view->place = $place; $this->view->title = $place->name; }
Holt Einträge aus Datenbank
Weist der View zu
}
Diese einfache Action holt den Eintrag für den Zielort aus der Datenbank und weist ihn der View zu. Die in Listing 5.9 gezeigte View-Datei application/views/scripts/place/ index.phtml gleicht den View-Skripts, die wir bereits gesehen haben. Listing 5.9 Das View-Skript von Place mit den Details der Zielorte und einer Liste der Rezensionen headScript()->appendFile($this->baseUrl().'/js/yahoo.js'); $this->headScript()->appendFile($this->baseUrl().'/js/connection.js'); $this->headScript()->appendFile($this->baseUrl().'/js/review_feedback.js'); ?>
Der erste Teil des View-Skripts fügt die erforderlichen JavaScript-Dateien der ViewHilfsklasse headScript() hinzu, die dann im -Abschnitt der Layout-Datei ausgegeben werden. Das View-Skript stellt dann die Informationen über den Zielort selbst dar. Die View-Hilfsklasse displayAddress() formatiert die Adressdaten des Zielorts für die Präsentation (falls Sie sich das genauer anschauen wollen: Sie finden alles im begleitenden Quellcode). Die Rezensionen selbst werden in einer Liste ausgegeben, für die ein separates View-Skript namens views/scripts/place/_reviewItem.phtml verwendet wird, das von der View-Hilfsklasse partialLoop() aufgerufen wird. Dies sehen Sie in Listing 5.10.
123
5 Ajax Listing 5.10 __reviewItem.phtml stellt eine Rezension dar.
escape($this->user_name); ?> on displayDate( $this->date_updated); ?> Formatiert Datum
mit der View–Hilfsklasse
displayDate() escape($this->body); ?>
Jede Rezension besteht aus dem Namen des Rezensenten und dem Datum, an dem sie verfasst wurde, gefolgt vom Rezensionstext selbst. Damit ist die Grundlage des für das Beurteilungssystem erforderlichen Codes gelegt.
5.5.2
Das View-Skript mit HTML fürs Rating ergänzen
Beim View-Skript, das die Rezensionen darstellt, müssen noch die Zählerinfo und die Schaltflächen für Yes und No ergänzt werden. Wir müssen auch darauf achten, dass wir alle HTML-Elemente identifizieren können, die dynamisch geändert werden sollen, indem wir ihnen eine eindeutige ID geben. Am einfachsten wird das mit einem String erledigt, dem die ID der Rezension folgt. All das kommt in ein separates Teil-View-Skript, das wir in das View-Skript _reviewItem.phtml integrieren. Das zusätzliche HTML für _reviewItem.phtml steht in Listing 5.11; das wird in den Code aus Listing 5.10 direkt vor dem schließenden
Tag eingebaut. Listing 5.11 Das HTML für das Feedback über die Rezensionen in _reviewItem.phtml
partial('place/_reviewFeedback.phtml', array('id'=>$this->id, Übergibt Variablen an 'helpful_yes'=>$this->helpful_yes, partielles View-Skript 'helpful_total'=>$this->helpful_total)) ?>
Mit dem partiellen View-Skript namens _reviewFeedback.phtml stellen wir das HTML für das Feedback dar. Dies wird im gleichen Verzeichnis wie das Skript _reviewItem.phtml gespeichert (siehe Listing 5.12).
124
5.5 Integration in eine Zend Framework-Applikation Listing 5.12 Das HTML für das Feedback über die Rezensionen in _reviewFeedback.phtml id; $yesCount = $this->helpful_yes; $totalCount = $this->helpful_total; ?>
Wie Sie in Listing 5.12 sehen können, besteht das HTML für das Feedbacksystem der Rezensionen aus einer Nachricht, die die Besucher darüber informiert, wie nützlich die Rezension eingeschätzt wird, zwei Buttons fürs Feedback (Yes oder No) und zwei Platzhalter für interaktive Reaktionen nach dem Klick auf einen Link. Damit dieser HTMLCode in einer Ajax-Applikation funktioniert, muss man auf jeden Teil von JavaScript aus gut zugreifen können. Dazu setzen wir das id-Attribut auf einen eindeutigen Wert und nutzen dafür die Datenbank-id der Rezension n. Die eigentliche Ajax-Arbeit erfolgt in der JavaScript-Klasse ReviewFeedback o, die aus ihrem Konstruktor heraus den Webserver kontaktiert. Dieses JavaScript schauen wir uns im Detail an, weil es die eigentliche Arbeit erledigt.
5.5.3
JavaScript in die View-Skripte einbauen
Wenn wir das YUI-Framework hinter den Kulissen für die Verbindungsaufnahme zum Server nutzen, bekommen wir obendrein eine Fehlerprüfung und eine browserübergreifende Kompatibilität! Die gesamte Aufgabe wird mit einer einzelnen JavaScript-Datei namens review_feedback.js erledigt, die sich im Verzeichnis www/js befindet. Diese wurde anhand der View-Hilfsklasse headScript() in index.phtml in Listing 5.9 hinzugefügt. Wir könnten die clientseitigen Aufgaben auf verschiedene, klar umrissene Aufgaben herunterbrechen: 1. Die Anfrage an den Server initiieren und eine „Bitte warten“-Animation („Spinner“) starten.
125
5 Ajax 2. Bei Erfolg:
Den Zähler aktualisieren. Die „Bitte warten“-Animation beenden. Dankeschön an den User. 3. Bei Fehlschlag:
Den User informieren. Die „Bitte warten“-Animation beenden. Um zu verhindern, dass der globale Namensraum vollläuft, legen wir den gesamten JavaScript-Code für das Feedback-Modul in eine Klasse namens ReviewFeedback. Der Konstruktor initialisiert alles und baut die Verbindung zum Server auf (siehe Listing 5.13). Listing 5.13 Der Klassenkonstruktor ReviewFeedback function ReviewFeedback(response, reviewId, baseUrl) { this.id = reviewId; this.baseUrl = baseUrl; this.startSpinner(); this.message("","");
Schaltet Animation ein und leert Informationsbereich
var response = parseInt(response); var reviewId = parseInt(reviewId);
Verwendet parseInt(), um Encoding der Parameter zu vermeiden
// perform the request. beim Server YAHOO.util.Connect.initHeader('X_REQUESTED_WITH', 'XMLHttpRequest'); YAHOO.util.Connect.asyncRequest('GET', sUrl, this);
}
Das Objektmodell von JavaScript unterscheidet sich ein wenig von dem bei PHP: Um eine Klasse zu erstellen, brauchen wir kein class-Schlüsselwort, sondern nur eine Funktion, die als Konstruktor dient. Die restlichen Methoden der Klasse werden dann der Eigenschaft prototype der Konstruktorfunktion hinzugefügt, über die sie dann für alle Instanzen der Klasse verfügbar ist. In Appendix B von Ajax in Action (von Dave Crane und Eric Pascarello) finden Sie weitere Einzelheiten, wenn Sie Ihre Kenntnisse über das JavaScriptObjektmodell auffrischen wollen. Wie Sie in Listing 5.13 sehen können, übergeben wir this als das Callback-Objekt n, wenn wir den Ajax-Aufruf mit asyncRequest initiieren. Das bedeutet, dass wir die Elementmethoden success() und failure() definieren müssen, damit sie vom YUI-System bei Bedarf aufgerufen werden können. Der Vorteil der Verwendung einer Instanz ist, dass wir die reviewId und die baseURL in der Instanz speichern können, und dass sie bei Bedarf in den Elementmethoden verfügbar sind. Wir können auch Hilfsklassenmethoden in der Klasse vorhalten, damit die wichtigen Callback-Methoden einfacher zu lesen sind. Wir müssen jeweils bei Erfolg und Fehlschlag einen Hinweis geben; also ist es sinnvoll, eine Methode namens message() zu schreiben:
126
5.5 Integration in eine Zend Framework-Applikation ReviewFeedback.prototype.message = function(class, text) { document.getElementById('message-'+this.id).className = class; document.getElementById('message-'+this.id).innerHTML = text; }
Die Dankeschön-Info wird im div mit der ID message-{review_id} dargestellt, und durch die Message-Methode brauchen wir nicht mehr weitschweifig die CSS-Klasse und den Text der auszugebenden Nachricht auszuführen. Außerdem brauchen wir noch zwei andere Hilfsklassenmethoden namens startSpinner()und stopSpinner(). Um eine solche „Bitte warten“-Animation zu erstellen, nehmen wir ein animiertes GIF, das bei startSpinner() ins HTML eingefügt und bei stopSpinner() entfernt wird: ReviewFeedback.prototype.startSpinner = function() { var spinner = document.getElementById('spinner-'+this.id); var url = this.baseUrl+'/img/spinner.gif'; spinner.innerHTML = ''; } ReviewFeedback.prototype.stopSpinner = function() { document.getElementById('spinner-'+this.id).innerHTML = ""; }
Wir können nun für die eigentliche Arbeit die Callback-Methode success() definieren (siehe Listing 5.14). Listing 5.14 Die JavaScript-Callback-Methode success() ReviewFeedback.prototype.success = function(o) { if(o.responseText !== undefined) { var json = eval("(" + o.responseText + ")") ; if(json.result && json.id == this.id) {
Prüft, ob dies die richtige Rezension ist
document.getElementById( Aktualisiert Zähler der Webseite 'counts-'+json.id).innerHTML = json.helpful_yes + ' of ' + json.helpful_total; this.message("success", Gibt Danke aus und 'Thank you for your feedback.'); stoppt Spinner this.stopSpinner(); document.getElementById( Entfernt Buttons, wenn 'yesno-'+json.id).innerHTML = ""; Feedback gegeben wurde } else { this.failure(o); failure()-Methode } }
erneut verwenden
}
Wie bereits angemerkt, muss die success-Methode drei verschiedene Dinge machen: den Zählertext für die Rezensionen aktualisieren, ein Dankeschön ausgeben und den Spinner stoppen. Wir entfernen auch die Buttons für Yes und No, weil sie nicht mehr benötigt werden. Um den Zählerstand zu aktualisieren, haben wir absichtlich einen <span> mit der ID helpful-{review_id} um die beiden Zahlen gelegt, die aktualisiert werden müssen. Wir setzen dann die innerHTML-Eigenschaft der beiden Spans, um den neuen Stand von „Yes“ und „Total“ zu aktualisieren. Um die Nachricht zu aktualisieren und den Spinner zu
127
5 Ajax stoppen, nehmen wir die bereits erstellten Hilfsklassenmethoden (so ist diese Methode leichter zu warten). Die Callback-Methode failure() ist ganz ähnlich, außer dass wir nur eine Nachricht ausgeben und den Spinner stoppen müssen: ReviewFeedback.prototype.failure = function(o) { var text = "Sorry, please try later."; this.message("failed", text); this.stopSpinner(); }
Mehr als dieses JavaScript ist für das Feedbacksystem nicht erforderlich. Der serverseitige Code ist in PHP geschrieben und ähnlich simpel.
5.5.4
Der Server-Code
Das clientseitige JavaScript ruft die Feedback-Action des Review-Controllers auf. Dies ist eine Standard-Action wie eine beliebige andere des Systems, und somit wird eine Klassenmethode namens ReviewController::feedbackAction() aufgerufen und in ReviewController.php gespeichert. Der wichtigste Unterschied zwischen einer Action, die auf eine Ajax-Anfrage reagiert, und einer Action, die HTML direkt darstellt, ist der View-Code; HTML wird nicht benötigt. Wir nehmen die Action-Hilfsklasse AjaxContext, damit das Layout-Rendering abgeschaltet und ein JSON-spezifisches View-Skript genutzt wird. Der Output der Action ist ein Array mit JSON-kodierten Daten, mit denen die success()Methode entsprechend den HTML-Text setzt. Das wird in der Methode init() der Klasse ReviewController eingerichtet, die in application/controllers/ReviewController.php gespeichert wird (siehe Listing 5.15). Listing 5.15 ReviewController::init() richtet den AjaxContext ein. class ReviewController extends Zend_Controller_Action { function init() { $ajaxContext = $this->_helper->getHelper('AjaxContext'); $ajaxContext->addActionContext('feedback', 'json'); $ajaxContext->initContext();
Akzeptiert JSON-Format als „Feedback“
}
Die init()-Methode ist im Wesentlichen die gleiche wie die bereits in Listing 5.6 untersuchte Methode. Der einzige Unterschied besteht darin, dass wir diesmal JSON als Format festlegen. Damit die Actions im ActionStack bei einer Ajax-Anfrage nicht aufgerufen werden, müssen wir den Places_Controller_Plugin_ActionSetup-Front-Controller so ändern, dass der Code in diesem Fall nicht greift. Das erreichen wir durch Umhüllen mit einer ifAbfrage: if(!$request->isXmlHttpRequest()) { ..... }
128
5.5 Integration in eine Zend Framework-Applikation Die Action-Methode für das Feedback steht in Listing 5.16. Listing 5.16 Die serverseitige Ajax-Antwort public function feedbackAction() { $id = (int)$this->getRequest()->getParam('id'); if ($id == 0) { $this->view->result = false; return; }
Garantiert, dass ID für SQL-Anweisungen sicher ist
Der Code in feedbackAction() ist Zend Framework-Standardcode, der die Zähler für die Datenbankfelder helpful_yes und helpful_total innerhalb der reviews-Tabelle für die jeweilige View aktualisiert. In diesem Fall konstruieren wir das SQL direkt und führen es über die query()-Methode des Zend_Db_Adapter-Objekts aus n. Beachten Sie, dass unbedingt darauf zu achten ist, eine Typumwandlung auf Integer durchzuführen, damit nicht unbeabsichtigt eine SQL Injection-Schwachstelle eingebaut wird, weil wir ja die Review-ID des Users direkt im SQL verwenden. Dann weisen wir die result-Variable und alle anderen Daten, die das JavaScript kennen soll, der View zu, damit es gleich im ViewSkript kodiert werden kann. Das View-Skript views/scripts/review/feedback.json.phtml steht in Listing 5.17.
129
5 Ajax Listing 5.17 Die Ausgabe der View-Variablen in feedback.json.phtml
Konvertiert öffentliche
Elementvariablen in Array
Das View-Skript für die Ajax-Antwort ist sehr einfach, weil der AjaxContext bereits die Hauptlast der Kodierung in JSON erledigt. Wir brauchen nur ein Array aller Daten im View auszugeben. Das macht man am einfachsten über get_object_vars()n. Wir haben nun den Zyklus einer Ajax-Anfrage im Kontext einer korrekten Zend Framework-Applikation vollständig abgeschlossen. Es gibt noch eine Reihe von Details, um die wir uns kümmern müssen, doch die Hauptarbeit, das alles zusammenzubinden, erledigt die Actionklasse AjaxContext des Zend Frameworks für uns. Unsere User können nun angeben, wie hilfreich eine Rezension ist, und diese Information können wir bei Bedarf zum Sortieren verwenden.
5.6
Zusammenfassung In diesem Kapitel beschäftigten wir uns mit Ajax und seiner Integration in einer Zend Framework-Applikation. Zwar ist es möglich, den gesamten erforderlichen Ajax-Code selbst zu schreiben, aber einfacher geht’s über eine der vielen verfügbaren Libraries, um die Entwicklung zu vereinfachen und den Code robuster zu machen. Als Beispiel nahmen wir uns die Yahoo! YUI-Libraries vor und lernten ihre einfache Verwendung kennen. Um das alles in einen Kontext zu bringen, integrierten wir außerdem einen Mechanismus, mit dem die User ein Feedback für hilfreiche Rezensionen auf der Places-Website abgeben können. Die Action-Hilfsklasse AjaxContext enthält einen Mechanismus, um Ajax-Aufrufe in einer Zend Framework-Applikation zu integrieren. Somit können mehrere View-Skripts mit einer Action verknüpft werden – eins für jedes Format einer Antwort. Am häufigsten kommen JSON, XML und einfaches HTML zum Einsatz. Die durch das Designpattern Model-View-Controller mögliche Trennung ist beim Schreiben von Ajax-Actions sehr hilfreich, weil Controller und Model von der View isoliert werden. Somit sind alle Grundlagen der Erstellung einer Zend Framework-Applikation und der Ausstattung des Frontends mit der neuesten Technologie gelegt. Im weiteren Verlauf beschäftigen wir uns detailliert mit der im Zend Framework enthaltenen Datenbankfunktionalität und wie Zend_Db_Table dabei helfen kann, Models zu erstellen, die sowohl leistungsfähig als auch einfach zu warten sind.
130
6 6
Mit der Datenbank arbeiten
Die Themen dieses Kapitels
Datenbank direkt mit Zend_Db abfragen Einführung in das Designpattern Table-Data-Gateway mit Zend_Db_Table Unit-Tests von Datenbankoperationen Beziehungen zwischen Datenbanktabellen Bei den meisten Websites ist eine Datenbank ein zentraler Bestandteil der Applikation. Beim Zend Framework wird dies entsprechend mit einem umfangreichen Satz datenbankrelevanter Komponenten berücksichtigt, die verschiedene Abstraktionsabstufungen ermöglichen. Zwei wichtige Stufen der Abstraktion sind Datenbanken und Tabellen. Bei der Datenbankabstraktion bleibt Ihr PHP-Code unabhängig vom zugrunde liegenden Datenbankserver. Somit kann Ihre Applikation leichter mehrere Datenbankserver unterstützen. Eine Tabellenabstraktion repräsentiert die Datenbanktabellen und -zeilen als PHP-Objekte. So kann Ihre restliche Applikation mit PHP zusammenarbeiten, ohne die zugrunde liegende Datenbank kennen zu müssen. Wir schauen uns zuerst die Klasse Zend_Db_Adapter an, die eine produktunabhängige Schnittstelle zum Datenbankserver bietet.
6.1
Datenbankabstraktion mit Zend_Db_Adapter Das Thema der Datenbankabstraktion scheint bei manchen quasireligiöse Dimensionen anzunehmen – viele Entwickler behaupten, dass solche Schichten zu einem PerformanceEngpass führen und dass sie keinen sinnvollen Zweck erfüllen. Dieser Glaube stammt von der Tatsache, dass man wissen muss, wie die Engine funktioniert, und dass man deren spezifische Interpretation von SQL einsetzen muss, um das Optimum herauszuholen. An-
131
6 Mit der Datenbank arbeiten deren Entwicklern würde es nicht im Traum einfallen, ohne Datenbankabstraktionsschicht zu arbeiten: Sie behaupten, dass man damit einfach von einer Datenbank zu einer anderen migrieren kann und somit die Distributionsmöglichkeiten ihrer Applikationen vielfältiger sind. Mit einer Datenbankabstraktionsschicht reicht es außerdem, dass der Entwickler für alle eingesetzten Datenbanken nur eine API lernen muss, was die Entwicklungszeit deutlich beschleunigen kann. Wie so oft bei solchen Debatten hängt alles davon ab, worin die Aufgabe besteht. Wenn Sie eine Applikation entwickeln (z. B. eine Suchmaschine wie Google), die aus der Datenbank die maximale Performance extrahieren muss, besteht der Königsweg eindeutig im Programmieren für genau eine Datenbank. Wenn Sie eine Applikation schreiben, die an Kunden verkauft werden soll, wird Ihr Produkt bessere Marktchancen haben, wenn es mehrere Datenbanken unterstützt. Es gibt nicht die eine richtige Antwort. In der Firma, für die Rob gerade arbeitet, haben wir Aufträge von Kunden, die nur mit SQL-Servern arbeiten und für die eine Verwendung von MySQL oder PostgreSQL inakzeptabel ist. Somit finden wir Datenbankabstraktionsschichten sehr praktisch, um die gleiche Software an mehrere Kunden verkaufen zu können. Die Schnittstelle des Zend Frameworks zur Datenbank verläuft über eine Abstraktionsschicht. Mit dem dadurch geschaffenen standardisierten Interface zur Datenbank wird gewährleistet, dass die Komponenten der höheren Ebenen mit jeder beliebigen Datenbank arbeiten können. Es bedeutet auch, dass der Support für eine bestimmte Datenbank an einem einzigen Punkt gekapselt ist, und somit die Unterstützung für andere bzw. neue DatenbankEngines einfacher wird. Schauen wir uns nun an, wie man bei Zend Framework anhand von Zend_Db_Adapter eine Datenbankverbindung aufbaut und wie man dann mit den Daten arbeitet.
6.1.1
Einen Zend_Db_Adapter erstellen
Der Kern der Datenbankabstraktionskomponenten im Zend Framework ist Zend_Db_ Adapter_Abstract. Jede unterstützte Datenbank-Engine enthält eine spezifische Adapterklasse, die von dieser Klasse erbt. Der Adapter von DB2 heißt beispielsweise Zend_Db_Adapter_Db2. Die PDO-Endung wird auch für manche Adapter verwendet, z. B. Zend_Db_Adapter_Pdo_Pgsql für PostgreSQL. Das Factory-Designpattern erstellt einen Datenbankadapter anhand der Methode Zend_Db::factory() (siehe Listing 6.1). Listing 6.1 Eine Zend_Db_Adapter-Instanz mit Zend_Db::factory() erstellen $params = array ('host' => 'localhost', 'username' => 'rob', 'password' => 'password', 'dbname' => 'places'); $db = Zend_Db::factory('PDO_MYSQL', $params);
132
Setzt spezifische Datenbank-EngineKonfiguration
Legt DatenbankEngine fest
6.1 Datenbankabstraktion mit Zend_Db_Adapter Weil jeder Adapter Zend_Db_Adapter_Abstract erweitert, können nur Methoden innerhalb der abstrakten Klasse verwendet werden, wenn Ihnen die Datenbankunabhängigkeit wichtig ist. Die Liste der unterstützten Datenbanken ist recht umfassend und schließt DB2, MySQL, Oracle, SQL Server und PostgreSQL ein. Wir gehen davon aus, dass im Laufe der Zeit auch andere Datenbanken unterstützt werden, vor allem jene, die auf PDO basieren. Die Datenbankadapter arbeiten mit der Lazy-Loading-Technik, um zu vermeiden, sich bei der Erstellung der Objektinstanz mit der Datenbank verbinden zu müssen. Das bedeutet, dass die Verbindung zur Datenbank erst dann aufgebaut wird, wenn Sie etwas mit dem Objekt machen, das eine Verbindung erfordert, auch wenn das Adapterobjekt anhand von Zend_Db::factory() erstellt wird. Der Aufruf der Methode getConnection() wird auch eine Verbindung erstellen, falls noch keine besteht. Nachdem wir nun die Verbindung zur Datenbank aufgebaut haben, schauen uns als Nächstes an, wie man daraus die Daten ausliest. Das kann über Standard-SQL-Abfragen erfolgen, aber einfacher geht es mit dem Zend_Db_Select-Objekt.
6.1.2
Die Datenbankabfrage
Eine SQL-Abfrage an eine Datenbank ist sehr einfach: $date = $db->quote('1980-01-01') $sql = 'SELECT * FROM users WHERE date_of_birth > ' . $date; $result = $db->query($sql);
Die Variable $result enthält ein Standard-PHP-PDOStatement-Objekt, und somit können Sie die Standardaufrufe wie fetch() oder fetchAll() verwenden, um die Daten auszulesen. In diesem Fall arbeiten wir mit einem Standard-SQL-String, um die Abfrage zu spezifizieren. Die Methode query() unterstützt auch das Binden von Parametern, um zu vermeiden, dass alle Strings mit quote() bearbeitet werden müssen. Dieses System erlaubt es, in der SQL-Anweisung dort mit Platzhaltern zu arbeiten, wo die Variablen sein sollen. Der Adapter (oder das zugrunde liegende PHP) sorgt dann dafür, dass unsere Abfrage valides SQL wird und keinen String enthält, der nicht korrekt in Anführungszeichen steht. Die vorige Abfrage könnte man also auch so schreiben: $sql = 'SELECT * FROM users WHERE date_of_birth > ?'; $result = $db->query($sql, array('1980-01-01'));
Als Faustregel sollte man sich parameterbasierte Abfragen angewöhnen, weil es so nicht passieren kann, dass man zufällig vergisst, Daten vom User mit quote() zu bearbeiten und möglicherweise eine SQL Injection-Schwachstelle in die Applikation einbaut. Das hat auch den positiven Nebeneffekt, dass es bei einigen Datenbanken schneller ist, mit gebundenen Datenparametern zu arbeiten.
133
6 Mit der Datenbank arbeiten Nicht jeder fühlt sich allerdings dabei wohl, komplexe SQL-Abfragen zu erstellen. Die Zend_Db_Select-Klassen des Zend Frameworks bieten ein auf PHP basierendes objektorientiertes Interface, um SQL-SELECT-Anweisungen zu generieren. 6.1.2.1
Zend_Db_Select
Mit Zend_Db_Select können Sie Abfrageanweisungen für Datenbanken ganz komfortabel mit PHP anstatt mit SQL erstellen. Zend_Db_Select
enthält verschiedene Vorteile, die wichtigsten folgen hier:
Metadaten werden automatisch in Anführungszeichen gesetzt (Tabellen- und Feldnamen).
Das objektorientierte Interface erleichtert die Wartung und Pflege. Datenbankunabhängige Abfragen werden einfacher. Wenn Werte in Anführungszeichen gesetzt werden, verringert das SQL InjectionSchwachstellen. Eine einfache Abfrage mit Zend_Db_Select steht in Listing 6.2. Listing 6.2 Eine Zend_Db_Adapter-Instanz mit Zend_Db::factory() erstellen $select = new Zend_Db_Select($db); $select->from('users'); ->where('date_of_birth > ?', '1980-01-01'); $result = $select->query();
Zur Objektauswahl Zend_Db_Adapter anhängen
Abfrage ausführen
Ein sehr praktisches Feature von Zend_Db_Select ist, dass Sie die Abfrage in beliebiger Reihenfolge aufbauen können. Das unterscheidet sich vom Standard-SQL insofern, dass Sie jeden Abschnitt Ihres SQL-Strings an der korrekten Stelle haben müssen. Bei einer komplexen Abfrage vereinfacht das die Wartung, weil durch Zend_Db_Select sichergestellt wird, dass die generierte SQL-Syntax korrekt ist. Neben Zend_Db_Select bietet der Zend_Db_Adapter Funktionen für direkte Schnittstellen mit der Datenbank, damit Sie Daten einfügen, löschen und aktualisieren können. Schauen wir uns an, wie das geht.
6.1.3
Einfügen, Aktualisieren und Löschen
Um Datenbankoperationen mit Datenbankzeilen zu vereinfachen, enthält Zend_Db_ die Methoden insert(), update() und delete().
Adapter
Einfügen und Aktualisieren arbeiten unter der gleichen grundlegenden Prämisse, dass Sie ein assoziatives Datenarray angeben und der Rest automatisch erledigt wird. Listing 6.3 zeigt, wie man einen User in eine Tabelle namens users einfügt.
134
6.1 Datenbankabstraktion mit Zend_Db_Adapter Listing 6.3 Über Zend_Db_Adapter eine Zeile einfügen // insert a user $data = array( 'date_created' => date('Y-m-d'), 'date_updated' => date('Y-m-d'), 'first_name' => 'Ben', 'surname' => 'Ramsey' );
Beachten Sie, dass die insert()-Methode des Zend_Db_Adapters automatisch dafür sorgt, dass Ihre Daten korrekt in Anführungszeichen gesetzt werden, weil es eine insertAnweisung erstellt, die mit Parameterbindung arbeitet (siehe Abschnitt 6.1.2). Die Aktualisierung eines Eintrags funktioniert anhand der update()-Methode sehr ähnlich, außer dass Sie eine SQL-Bedingung übergeben müssen, um die Aktualisierung nur auf die gewünschten Zeilen beschränken. Normalerweise kennen Sie die ID der Zeile, die aktualisiert werden soll, doch update() ist flexibel genug, um mehrere Zeilen gleichzeitig aktualisieren zu können (siehe Listing 6.4). Listing 6.4 Mehrere Zeilen mit Zend_Db_Adapter aktualisieren // update a user $data = array( 'country' => 'United Kingdom' );
Legt fest, welche Felder aktualisiert werden sollen
Auch jetzt setzt update() die Daten im $data-Array automatisch in Anführungszeichen, macht aber mit den Daten in der $where-Bedingung nicht automatisch das Gleiche. Also nehmen wir quoteInto(), damit wir mit garantiert sicheren Daten in der Datenbank arbeiten können. Das Löschen in der Datenbank funktioniert auf die gleiche Weise, aber wir brauchen die Tabelle und die $where-Bedingung, und somit sieht der Code zum Löschen eines bestimmten Users etwa wie folgt aus: $table = 'users'; $where = 'id = 1'; $rows_affected = $db->delete($table, $where);
Natürlich müssen wir auch hier wieder die Strings in der $where-Bedingung in Anführungszeichen setzen. In allen Fällen geben insert(), update() und delete() die Anzahl der von der Operation betroffenen Zeilen zurück. Wir schauen uns nun an, wie man die datenbankspezifischen Unterschiede bewältigt.
135
6 Mit der Datenbank arbeiten
6.1.4
Spezifische Unterschiede zwischen den Datenbanken
Datenbanken sind nicht alle gleich, und das gilt vor allem für ihre Interpretation von SQL und die zusätzlichen Funktionen, über die man auf die fortgeschritteneren Features der Datenbank zugreifen kann. Um die spezifischen SQL-Funktionen Ihres Datenbankservers zugreifen zu können, verfügt die Zend_Db_Select-Klasse über eine Hilfsklasse namens Zend_Db_Expr. Mit Zend_Db_Expr werden SQL-Funktionen aufgerufen oder andere Ausdrücke zur Nutzung in SQL erstellt. Schauen wir uns ein Beispiel an, bei dem der Vor- und Nachname eines Users konkateniert (verkettet) werden soll. Der Code steht in Listing 6.5. Listing 6.5 Funktionen in SQL-Anweisungen $select = $db->select(); $columns = array(id, "CONCAT(first_name, ' ', last_name) as n"; $select->from('users', $columns); $stmt = $db->query($select); $result = $stmt->fetchAll();
Verwendet das MySQL-spezifische CONCAT()
Die from()-Methode erkennt, dass im columns-Parameter eine Klammer verwendet wurde, und konvertiert es somit automatisch in eine Zend_Db_Expr. Wir könnten auch Zend_Db_Expr direkt verwenden, indem die columns-Anweisung explizit gesetzt wird: $columns = array(id, "n"=> new Zend_Db_Expr("CONCAT(first_name, ' ', last_name"));
Nachdem wir uns angeschaut haben, wie man die Unterschiede zwischen den DatenbankEngines außer Acht lassen kann, wenn man mit anhand der Factory-Methode Zend_Db erstellten Adaptern arbeitet, können wir uns dem Thema zuwenden, wie eine Datenbank in einer Applikation eingesetzt wird. Novizen unter den Webprogrammierern neigen dazu, die Datenbankaufrufe immer genau dort abzulegen, wo sie sie benötigen. Aber dadurch werden Wartungsarbeiten zu einem Alptraum, weil die SQL-Anweisungen in der ganzen Applikation verstreut sind. Wir schauen uns an, wie man das SQL konsolidieren kann und wie die Zend Framework-Komponente Zend_Db_Table dabei hilft, die Architektur unserer Applikationen zu verbessern.
6.2
Tabellenabstraktion mit Zend_Db_Table Beim Umgang mit einer Datenbank ist es sehr hilfreich, das eigene Denken abstrahieren zu können und sich das System auf Domänenebene vorzustellen, anstatt sich auf das Wesentliche der eigentlichen SQL-Anweisungen zu konzentrieren. Auf der Domänenebene können Sie in der Sprache der Domäne über das Problem nachdenken. Am einfachsten geht das, wenn Sie Klassen erstellen, die wissen, wie sie sich selbst in der Datenbank speichern und daraus laden können. Eine Klasse, die eine Zeile in einer Daten-
136
6.2 Tabellenabstraktion mit Zend_Db_Table banktabelle repräsentiert, implementiert das Designpattern Row-Data-Gateway. Mit diesem Pattern kann auf eine einzelne Zeile der Datenbank zugegriffen werden, und es ist eng verwandt mit dem Active-Record-Pattern. Unterschiede zwischen Active-Record und Row-Data-Gateway Das Active-Record-Pattern ist eng mit Row-Data-Gateway verwandt. Die wesentlichen Unterschiede bestehen darin, dass das Row-Data-Gateway nur Funktionen für den Datenbankzugriff enthält, Active-Record hingegen auch Domänenlogik. Bei ActiveRecord-Implementierungen sind meist auch statische Finder-Funktionen enthalten, aber das ist keine Anforderung der Pattern-Definition. Beim Zend Framework wird Zend_Db_Table_Row_Abstract erweitert, um in der Klasse die Domänenlogik zu implementieren, also verwandeln wir das enthaltene Row-DataGateway in speziellen Applikationen eher in eine Art Active-Record. Das Row-Data-Gateway-Pattern funktioniert im Bereich von Listen nicht so gut, wenn Sie also beispielsweise eine Produktliste aus einer E-Commerce-Applikation auslesen wollen. Das liegt daran, dass es auf Zeilenebene arbeitet, und Listen generell auf Tabellenebene bearbeitet werden. Das Zend Framework enthält die Komponente Zend_Db_Table, um Datenbankmanipulationen auf Tabellenebene zu unterstützen, und wir schauen uns an, was darin enthalten ist und wie sich Unterstützung auf Tabellenebene von dem unterscheidet, was wir bereits im Zend_Db_Adapter gesehen haben.
6.2.1
Was ist das Table-Data-Gateway-Pattern?
besteht aus drei Hauptkomponenten: Zend_Db_Table_Abstract, Zend_Db_ und Zend_Db_Table_Row. Wie der Name von Zend_Db_Table_Abstract schon andeutet, handelt es sich um eine abstrakte Klasse, die für jede Tabelle erweitert werden muss, für die es als Gateway dienen soll. Zend_Db_Table
Table_Rowset
Wenn mehrere Einträge aus der Tabelle ausgewählt werden, wird eine Instanz von Zend_Db_Table_Rowset zurückgegeben, durch die dann iteriert wird, um auf jede einzelne Zeile zuzugreifen. Jede Zeile ist eine Instanz von Zend_Db_Table_Row, das wiederum selbst eine Implementierung des Row-Data-Gateway-Patterns ist. Dies wird in Abbildung 6.1 gezeigt.
137
6 Mit der Datenbank arbeiten Zend_Db_Table_Abstract
Zend_Db_Table enthält eine saubere Methode zur Verwaltung von Datenbanktabellen und den damit verknüpften Zeilen.
Zend_Db_Table_Abstract wird immer erweitert, doch bei Zend_Db_Table_Rowset ist das weitaus weniger häufig der Fall, weil darin schon das Meiste enthalten ist, was Sie für den Umgang mit Datenbankzeilen benötigen. In komplizierteren Systemen kann es hilfreich sein, wenn man Zend_Db_Table_Row mehr in Richtung Active-Record erweitert, indem man der Klasse Domänenlogik hinzufügt, um zu verhindern, dass Code mehrfach an verschiedenen Stellen abgelegt wird. Das werden wir sehen, wenn es gleich um Zend_Db_Table geht.
6.2.2
Die Arbeit mit Zend_Db_Table
Um die Komponenten Zend_Db_Table tatsächlich nutzen zu können, müssen Sie eine Klasse erstellen, um Ihre Tabelle zu repräsentieren (siehe Listing 6.6). Diese wird meist genauso benannt wie die Datenbanktabelle, auf die sie zugreift, was aber nicht zwingend erforderlich ist. Listing 6.6 Eine Tabellenklasse zur Nutzung mit Zend_Db_Table deklarieren class Users extends Zend_Db_Table_Abstract { protected $_name = 'users'; }
Wie Sie in Listing 6.6 sehen, haben wir den Namen der zugrunde liegenden Tabelle explizit auf users gesetzt. Wenn die Eigenschaft $_name nicht gesetzt wird, wird stattdessen (unter Beibehaltung der Groß/Kleinschreibung) der Name der Klasse verwendet. Weil es allerdings eine übliche Praxis ist, bei den Klassennamen mit einem Großbuchstaben zu beginnen und die Namen der Datenbanktabellen nur klein zu schreiben, wird der Datenbanktabellenname meist explizit deklariert.
138
6.2 Tabellenabstraktion mit Zend_Db_Table Da wir nun die Klasse Users haben, können wir Daten aus der Tabelle auf genau die gleiche Weise auslesen, als würden wir Daten aus einer Instanz von Zend_Db_Adapter holen: $users = new Users(); $rows = $users->fetchAll();
Obwohl sie die gleiche Funktion ausführt, wird die fetchAll()-Methode von Zend_Db_Table_Abstracts völlig anders eingesetzt als die fetchAll()-Version von Zend_Db_Adapter_Abstract, die einfach einen SQL-String-Parameter bekommt und einen Array aus Arrays zurückgibt. Weil wir mit der Tabelle auf einer höheren Abstraktionsstufe arbeiten, arbeitet fetchAll() ebenfalls auf der höheren Stufe, wie Sie dessen Signatur entnehmen können: public function fetchAll($where = null, $order = null, $count = null, $offset = null)
Die Methode fetchAll() wird die SQL-Anweisung für uns erstellen und dafür intern Zend_Db_Table_Select verwenden; wir müssen nur noch die Teile angeben, an denen wir interessiert sind. Um beispielsweise alle weiblichen User auszuwählen, die nach 1980 geboren wurden, müssten wir den folgenden Code einsetzen: $users = new Users(); $where = array('sex = ?' => 'F', 'date_of_birth >= ?'=>'1980-01-01'); $rows = $users->fetchAll($where);
Wir können auch ein Zend_Db_Table_Select-Objekt so wie folgt verwenden: $users = new Users(); $select = $users->select(); $select ->where('sex = ?', 'F'); $select ->where('date_of_birth >= ?', '1980-01-01'); $rows = $users->fetchAll($select);
In diesem Beispiel erstellen wir die Abfrage in genau der gleichen Weise, als wenn wir mit einem Zend_Db_Select-Objekt arbeiten, und so können wir die Abfrage außer der Reihe erstellen, falls das nötig ist. Wir können beispielsweise ein Limit setzen, bevor wir die $where-Bedingungen setzen. Nachdem wir nun wissen, wie man mittels Zend_Db_Table Daten aus der Datenbank ausliest, schauen wir uns an, wie man die Daten anhand der Methoden insert() und update() einfügt und bearbeitet.
6.2.3
Einfügen und Aktualisieren mit Zend_Db_Table
Das Einfügen und Aktualisieren mit Zend_Db_Table ähnelt sehr der Arbeit mit Zend_Db_ außer dass wir dieses Mal bereits wissen, welche Datenbanktabelle wir nehmen. In Tabelle 6.1 wird gezeigt, wie ähnlich das Einfügen mit den beiden Komponenten ist. Adapter,
139
6 Mit der Datenbank arbeiten Tabelle 6.1 Vergleich Einfügen bei Zend_Db_Table und bei Zend_Db_Adapter Zend_Db_Adapter
Wie Sie der Tabelle 6.1 entnehmen können, kommt in beiden Fällen genau der gleiche Prozess zum Tragen, außer dass wir bei Zend_Db_Table bereits den Namen der Tabelle kennen, und dass wir die ID des neu eingefügten Eintrags direkt zurückbekommen. Die Aktualisierung einer Datenbankzeile ist ähnlich – siehe Tabelle 6.2. Tabelle 6.2 Vergleich Aktualisieren bei Zend_Db_Table und bei Zend_Db_Adapter Zend_Db_Adapter
Beachten Sie, dass alle Werte und Identifikatoren im SQL-Ausdruck, die für die $whereBedingung verwendet werden, in Anführungszeichen gesetzt werden müssen. Dafür sorgen die Methoden quote(), quoteInto() und quoteIdentifier() des Datenbankadapters. Die Ähnlichkeiten zwischen Einfügen und Aktualisieren führen zu dem Schluss, dass es praktisch wäre, wenn man nur eine Speichermethode in Zend_Db_Table hätte. Das wäre auf Tabellenebene inkorrekt, weil man ja niemals weiß, welcher Eintrag (oder welche Einträge) aktualisiert werden müssen. Auf der Ebene von Zend_Db_Table_Row ist das sehr sinnvoll. Mit Blick auf Zend_Db_Table_Row entdecken wir, dass uns diese Funktionalität bereits in einer Methode namens save() zur Verfügung steht. Deren Nutzung zeigt Tabelle 6.3.
140
6.2 Tabellenabstraktion mit Zend_Db_Table Tabelle 6.3 Daten mit der save()-Methode von Zend_Db_Table_Row speichern Einen neuen Eintrag einfügen
Einen Eintrag aktualisieren
$users = new Users(); $row = $users->fetchNew();
$users = new Users(); $row = $users->fetchRow('id=2');
In diesem Fall sind alle Unterschiede zwischen dem Einfügen eines neuen Eintrags und der Aktualisierung eines vorhandenen versteckt. Das liegt daran, dass das Zend_Db_Table_ Row-Objekt auf einer höheren Abstraktionsebene arbeitet.
6.2.4
Einträge mit Zend_Db_Table löschen
Wie zu erwarten ist, funktioniert das Löschen auf genau die gleiche Weise wie Einfügen und Aktualisieren, nur dass wir mit der delete()-Methode arbeiten. Diese gibt es sowohl in Zend_Db als auch in Zend_Db_Table, und wie bei insert() und update() ist die Verwendung bei den beiden Klassen sehr ähnlich (siehe Tabelle 6.4). Tabelle 6.4 Vergleich Löschen von Zeilen bei Zend_Db_Table und bei Zend_Db_Adapter Zend_Db_Adapter
Zend_Db_Table
// Einen User löschen $table = 'users';
// Einen User löschen $users = new Users();
$where = 'id = 2'; $db->delete($table, $where);
$where = 'id = 2'; $users->delete($where);
Die $where-Bedingung bei delete() setzt weder in Zend_Db_Table noch in Zend_Db_ die Werte für uns in Anführungszeichen. Wenn wir also Zeilen löschen müssten, die nicht auf einem Integer basieren, müssten wir mit quote() oder quoteInto() arbeiten. Um beispielsweise alle in London lebenden User zu löschen, müssten wir Folgendes schreiben: Adapter
Wir kennen nun alle wichtigen Datenbankoperationen, die man mit den Komponenten Zend_Db_Adapter und Zend_Db_Table durchführen kann. Datenbankoperationen in einer Model-View-Controller-Applikation sind normalerweise Teil des Models. Das liegt daran, dass sich die Business-Logik der Applikation im Model befindet, und die in einer Datenbank gespeicherten Daten sehr eng zum Business der Applikation gehören.
141
6 Mit der Datenbank arbeiten Wir schauen uns nun an, wie man mit auf Zend_Db_Table basierenden Objekten in einem Model arbeitet und (was wahrscheinlich noch wichtiger ist) wie man solche Objekte testet.
6.3
Zend_Db_Table als Model In Zend_Db_Table integrieren wir normalerweise die Zend_Db-Komponente in einer Zend Framework-MVC-Applikation. Man kann z. B. ein Model implementieren, indem man einen Set Klassen erstellt, die Zend_Db_Table_Abstract und Zend_Db_Table_Row_ Abstract erweitern. Bei der Places-Webapplikation nehmen wir ein Model, um die registrierten User der Website zu repräsentieren. Die dafür erforderliche Datenbanktabelle ist recht einfach (siehe Abbildung 6.2). users +id: int +date_created: datetime +date_updated: datetime +username: varchar(100) +password: varchar(40) +first_name: varchar(100) +last_name: varchar(100) +date_of_birth: date +sex: char(1) +postcode: varchar(20)
Abbildung 6.2 Die Tabelle users für Places enthält Login-Informationen und demographische Informationen für Anzeigenkunden
Weil wir mit Places Geld verdienen wollen, gehen wir davon aus, dass die Anzeigenkunden demographische Daten über die Mitgliedschaft haben wollen. Somit erwarten wir von unseren Mitgliedern, dass sie uns ihr Alter, ihr Geschlecht und ihren Wohnort verraten. Natürlich sind diese Angaben freiwillig, aber die Angabe eines Usernamens und das Passwort sind verpflichtend. Ein anspruchsvolleres Model könnte sogar die demographischen Angaben in einer separaten Tabelle ablegen. Wir müssen auf unsere Datenbanktabellen auf zwei unterschiedlichen Ebenen zugreifen: Tabelle und Zeile. Die Tabellenebene wird verwendet, wenn wir Listen darstellen müssen, und auf der Zeilenebene geht es um einzelne Einträge. Um das im Code abzubilden, brauchen wir die beiden Klassen Users und User, die Erweiterungen von Zend_Db_Table_ Abstract bzw. Zend_Db_Table_Row_Abstract sind. Die Klassendefinitionen stehen in Listing 6.7. Beachten Sie, dass Sie normalerweise im Verzeichnis application/models mit zwei Dateien für die beiden Klassen arbeiten würden, und zwar Users.php und User.php, weil damit die Applikation einfacher zu pflegen ist.
142
6.3 Zend_Db_Table als Model Listing 6.7 Ein Model anhand der Zend_Db_Table-Komponenten erstellen class Users extends Zend_Db_Table_Abstract { Setzt generierte protected $_name = 'users'; Zeilenklassen auf Typ User protected $_rowClass = 'User'; }
class User extends Zend_Db_Table_Row_Abstract { }
Die Verlinkung der User-Klasse mit der Klasse Users erfolgt über die Eigenschaft $_rowClass in der Users-Klasse n. Das bedeutet, dass die Klasse Zend_Db_Table_ Abstract Objekte des Typs User erstellen wird, wo sie sonst normalerweise ein Objekt des Typs Zend_Db_Table_Row erstellen würde. Die Implementierung der User-Klasse in Listing 6.7 ist nicht hilfreicher als die StandardZend_Db_Table_Row, weil keine weitere Funktionalität dabei herausspringt. Um unsere User-Klasse nützlich zu machen, werden wir eine neue Methode namens name einfügen. Damit wird applikationsweit der Name des Users dargestellt. Sie wird anfänglich den Vorund Nachnamen kombiniert darstellen, aber wenn diese nicht angegeben sind, wird sie den Usernamen des Anwenders nehmen. Die anfängliche Implementierung der Methode User::name() ist wie folgt: Class User extends Zend_Db_Table_Row_Abstract { public function name() { $name = trim($this->first_name . ' ' . $this->last_name); if (empty($name)) { $name = $this->username; } return $name; } }
Wir können unsere neue Methode name() direkt verwenden: $rob = $users->find(1)->current(); echo $rob->name();
Es wäre schöner, wenn wir den Namen als eine Nur-lesen-Eigenschaft des Eintrags behandeln können, damit sie genauso verwendet wird wie beispielsweise das Geburtsdatum in date_of_birth. Das wird die Vorhersagbarkeit der Klasse erhöhen, weil der Programmierer nicht darüber nachdenken muss, ob eine bestimmte Eigenschaft ein Feld in der Datenbank oder eine Funktion innerhalb der Klasse ist. Das geht am einfachsten und effektivsten durch Überschreiben von __get() (siehe Listing 6.8).
143
6 Mit der Datenbank arbeiten Listing 6.8 Durch Überschreiben von __get() werden eigene Eigenschaften möglich. class User extends Zend_Db_Table_Row_Abstract { //... Prüft, ob function __get($key) Ruft Methode Klassenmethode auf und gibt { existiert if(method_exists($this, $key)) zurück, falls { Methode existiert return $this->$key(); } return parent::__get($key); Ruft übergeordnetes __get() auf, } falls Methode nicht existiert //... }
Wir müssen die Methode __get() des Elternelements zuletzt aufrufen, weil eine Exception geworfen wird, wenn in der Datenbank $key als Feld nicht existiert. Beachten Sie, dass diese Methode aufgerufen und nicht der Wert des Feldes zurückgegeben wird, wenn eine der Methoden in der Klasse den gleichen Namen trägt wie ein Feld in der Datenbank. Das ist praktisch, wenn bei einem bestimmten Feld Bearbeitungsvorgänge vorgenommen werden sollen, bevor es an die restliche Applikation übergeben wird, obwohl ein solcher Use Case recht selten ist. Wir können nun unsere Models erstellen, müssen aber sicher sein, dass sie funktionieren. Das merken wir am besten, wenn wir den Code mit einem Testsystem testen.
6.3.1
Das Model testen
Wie bereits erwähnt, ist es eine gute Praxis, den eigenen Code zu testen, und eine noch bessere, ihn öfter als nur einmal zu testen! Um die Model-Klasse Users zu testen, nehmen wir PHPUnit, weil auch das Zend Framework selbst damit arbeitet. Die Tests werden im Verzeichnis tests/models von Places gespeichert. Wir beginnen unsere Tests, indem wir sicherstellen, dass die Testumgebung bei allen Tests konsistent ist, was in Testerkreisen als Setup und Teardown (etwa: Einrichten und Abreißen) bezeichnet wird. 6.3.1.1
Setup und Teardown
Eine zentrale Anforderung bei automatisch ablaufenden Tests ist, dass sie sich nicht gegenseitig beeinflussen dürfen. Ein bestimmter Test darf außerdem nicht davon abhängig sein, dass ein voriger Test durchgeführt worden ist. Um diese Trennung zu erzielen, enthält PHPUnit die Methoden setUp() und tearDown(), die direkt vor und nach jedem Test gestartet werden. Um alle Datenbanktests voneinander zu trennen, nehmen wir die Methode setUp(), um die Datenbanktabellen neu zu erstellen und sie in einem bekannten Zustand zu befüllen. Das heißt natürlich, dass wir bei den Tests nicht die Master-Datenbank verwenden! Beim
144
6.3 Zend_Db_Table als Model Testen nehmen wir eine andere Datenbank, die in der config.ini der Applikation konfiguriert wird. Somit können wir genau steuern, welche Daten sich darin befinden. Wir nehmen den Abschnitt [test], um die Testdatenbank anzugeben (siehe Listing 6.9). Listing 6.9 Der Abschnitt [test] überschreibt [general] in application/config.ini. [general] db.adapter = PDO_MYSQL db.config.host = localhost db.config.username = zfia db.config.password = 123456 db.config.dbname = places [test : general] db.config.dbname = places_test
Enthält Informationen zum Verbindungsaufbau mit der Haupt-Datenbank Überschreibt [general]
Überschreibt Datenbanknamen beim Testen
In unserem Fall müssen wir nur den Datenbanknamen von places zu places_test ändern; wir können uns hinsichtlich der anderen Verbindungsinformationen auf die anderen Einstellungen im Abschnitt [general] verlassen. Nun können wir das Grundgerüst für die Unit-Test-Klasse schaffen (siehe Listing 6.10). Listing 6.10 Grundgerüst für eine Unit-Test-Klasse: tests/models/UsersTests.php
Setzt Pfad
// set up database $dbConfig = $config->db->config->asArray(); $db = Zend_Db::factory($config->db->adapter, $dbConfig); Zend_Db_Table::setDefaultAdapter($db); Zend_Registry::set('db', $db); $this->db = $db; }
Baut Verbindung zur Datenbank auf und speichert Adapter zur späteren Verwendung
} }
Dieser Abschnitt der Unit-Test-Datei enthält nur den Initialisierungscode, der einmal gestartet wird. Wie Sie ganz oben sehen n, müssen wir darauf achten, dass wir die Pfade korrekt setzen, damit die Dateien gefunden werden können, und wir müssen auch alle erforderlichen Dateien einbinden. Die Definition von ROOT_DIR wird geprüft, um sicher zu sein, dass das nicht bereits definiert worden ist. Somit können wir unsere Tests auch in Zukunft erweitern, indem wir diese Unit-Test-Datei in einer Suite mit Dateien platzieren, die alle gleichzeitig getestet werden können; wenn wir den Test als Teil einer Suite durchführen, wollen wir nicht den Pfad ändern müssen. Wir binden auch die Dateien aus dem Framework ein, die wir für diesen Test benötigen. Weil der Konstruktor nur einmal aufgerufen wird, ist das die ideale Stelle, um die config.ini-Datei zu laden und die Verbindung zur Datenbank aufzunehmen. Auch hier: Wenn diese Klasse Teil einer Test-Suite ist, wird die Datenbankverbindung bereits aufgebaut sein. In diesem Fall beziehen wir sie einfach aus der Registry. Anderenfalls setzen wir den Datenbankadapter als Standardadapter für Zend_Db_Table und speichern in der Registry, nachdem die Verbindung aufgebaut wurde. Wir weisen ihn auch als eine Klassenelementvariable zu, weil er in der setUp()-Methode dazu benutzt wird, unsere Datenbank in einen bekannten Zustand zu versetzen. Bei der Initialisierung der Datenbank wird dann nur noch über den Datenbankadapter die Tabelle erstellt und einige Zeilen eingefügt. Dafür sorgt eine Methode namens _setupDatabase(), die von setUp() aufgerufen wird (siehe Listing 6.11). Listing 6.11 Initialisierung der Datenbank in setUp()
public function setUp() Ruft vor jedem { Unit-Test setUp() auf // reset database to known state $this->_setupDatabase(); } protected function _setupDatabase() { $this->db->query('DROP TABLE IF EXISTS users;');
146
Löscht Tabelle, falls vorhanden
6.3 Zend_Db_Table als Model $this->db->query(<<<EOT CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, date_created DATETIME, date_updated DATETIME, username VARCHAR(100) NOT NULL, password VARCHAR(40) NOT NULL, first_name VARCHAR(100), last_name VARCHAR(100), email VARCHAR(150) NOT NULL, town VARCHAR(100), country VARCHAR(100), date_of_birth datetime, sex char(1), postcode VARCHAR(30) ) EOT );
Dieser Code ist sehr einfach und sorgt nur dafür, dass unsere Datenbank für jeden Test korrekt eingerichtet ist. Die Tabelle wird gelöscht und eine neue erstellt, dann werden die Datenbankzeilen eingefügt (dafür wird die insert()-Methode des Datenbankadapters genutzt, die bereits angesprochen wurde). Anmerkung
Der Befehl DROP TABLE in Listing 6.11 ist spezifisch für MySQL und muss ggf. bei anderen Datenbank-Engines verändert werden.
Nun sind wir in der Lage, die eigentlichen Tests zu schreiben, um zu wissen, ob unser Model auch wie erwartet funktioniert! 6.3.1.2
Testen der Klasse Users
Die Klasse Users überschreibt die Methoden insert() und update(), um sicher zu sein, dass die Felder date_created und date_updated ausgefüllt sind, ohne dass sich der restliche Code darum kümmern muss. Die Unit-Tests zur Prüfung, ob dieser Code funktioniert,
147
6 Mit der Datenbank arbeiten stehen in Listing 6.12. Wir haben die Tests in zwei separate Methoden aufgeteilt: einen für das Einfügen und einen für das Aktualisieren. Listing 6.12 Unit-Tests für das Users-Model public function testInsertShouldFillInDateCreatedAndUpdatedFields() { Erstellt neues, $users = new Users(); leeres Zeilenobjekt $newUser = $users->fetchNew(); $newUser->first_name = 'Nick'; $newUser->last_name = 'Lo'; $newUser->password = 'nick'; $newUser->email = '[email protected]';
Stellt einige Daten ein Fügt Daten in Datenbank mit save() ein
// check that the date_created has been filled in $this->assertNotNull($nick->date_created); // check that the date_updated has been filled in $this->assertSame($nick->date_updated, $nick->date_created); }
Prüft, ob date_
created valide ist
Prüft, ob date_updated
gleich date_created ist
public function testUpdateShouldUpdateDateUpdatedField() { $users = new Users(); $rob = $users->find(1)->current(); Bearbeitet eine Zeile, $rob->town = 'Worcester'; um eine Änderung zu erstellen $rob->save(); $rob2 = $users->find(1)->current(); $this->assertTrue( ($rob2->date_updated > $rob2->date_created));
Prüft, ob date_updated neuer ist als date_created
}
Innerhalb von testInsertShouldFillInDateCreatedAndUpdatedFields() gibt es eine ganze Reihe von Setup-Code. Bevor wir die eigentlichen Tests starten (n, o), die wir durchführen wollen, müssen wir einen neuen Eintrag in der Datenbank eingefügt haben. Wie Sie sehen, gibt die fetchNew()-Methode von Zend_Db_Table ein leeres Objekt des Typs User zurück, das eine Zeile in der Datenbank repräsentiert. Wir können dann die Felder setzen, die wir brauchen, und sie mittels der Methode save() in die Datenbank einfügen. Die save()-Methode der Zeile arbeitet je nach dem, was passend ist, mit der verknüpften Methode insert() oder update() von Zend_Db_Table_Abstract, die dann mit den überschriebenen Methoden sicherstellt, dass die Felder date_created und date_updated korrekt gefüllt sind. Die Methode testUpdateShouldUpdateDateUpdatedField() führt einen ähnlichen Test durch, doch hier bearbeiten wir einfach eine der Zeilen in der Datenbank, die durch die
148
6.3 Zend_Db_Table als Model setUp()-Methode
erstellt wurde, um zu prüfen, dass eine Aktualisierung das date_updated-Feld verändert. Es ist ganz wichtig, die Zeile noch einmal aus der Datenbank zu bekommen, um sicher zu sein, dass das Feld date_updated in der Datenbank selbst auch geändert wurde. Nachdem wir nun die Klasse Zend_Db_Table und deren verknüpften Zeilensatz und Zeilenklassen untersucht haben, können wir uns anschauen, wie Tabellen zusammengeführt werden.
6.3.2
Tabellenbeziehungen mit Zend_Db_Table
Da, wo die Tabellen aufeinander bezogen werden, wird Zend_Db_Table erst richtig interessant. Das ist der Bereich, in dem Tabellen mit SQL-JOIN-Anweisungen verlinkt werden, und hier ist es auch sehr schön, dass das Framework die Fleißarbeit für uns erledigt. Wir schauen uns an, wie Zend_Db_Table uns bei den one-to-many- und many-to-manyBeziehungen helfen kann. 6.3.2.1
One-to-many-Beziehungen
Uns sind one-to-many-Beziehungen schon in Kapitel 3 begegnet, als wir das erste Schema für die Places-Applikation erstellt haben. Jeder Zielort kann viele Rezensionen haben. Dies sehen Sie in Ábbildung 6.3. places +id: int
reviews
+date_created: datetime
+id: int
+date_updated: datetime +name: varchar(100)
+date_created: datetime place_id
+date_updated: int
+address1: varchar(100)
+place_id: int
+address2: varchar(100)
+user_id: int
+address3: varchar(100)
+body: mediumtext
+town: varchar(75)
+rating: int
+county: varchar(75)
+helpful_yes: int
+postcode: varchar(30)
+helpful_total: int
+country: varchar(75)
Abbildung 6.3 Eine one-to-many-Beziehung wird über einen Fremdschlüssel (place_id) in der reviews-Tabelle erstellt.
Um das anhand von Zend_Db_Table zu repräsentieren, erstellen wir zwei neue Klassen, die von Zend_Db_Table_Abstract erweitert werden, und nehmen dann die Eigenschaften $_dependantTables und $_referenceMap, um sie miteinander zu verlinken. Dies sehen Sie in Listing 6.13.
149
6 Mit der Datenbank arbeiten Listing 6.13 one-to-many-Beziehung mit Zend_Db_Table class Places extends Zend_Db_Table_Abstract Gibt die { Datenbanktabelle an protected $_name = 'places'; protected $_dependentTables = array('Reviews'); Spezifiziert die ab}
Um eine one-to-many-Beziehung zu definieren, wird die $_referenceMap genommen. Jede Beziehung hat ihr eigenes Unterarray n, und um eine maximale Flexibilität zu sichern, sagen Sie der Klasse, welche Spalten genau in der jeder Tabelle verwendet werden. In unserem Fall hat die reviews-Tabelle eine Spalte namens places_id, die mit der idSpalte in der places-Tabelle verlinkt ist. Anmerkung
Beide Einträge der Spalten in der Referenzmap werden als Arrays definiert. So können zusammengesetzte Schlüssel (composite keys) mehr als ein Tabellenfeld enthalten.
Um dann die Rezensionen für einen bestimmten Eintrag eines Zielortes auszulesen, wird die Methode findDependentRowset() verwendet: $londonZoo = $places->fetchRow('id = 1'); $reviews = $londonZoo->findDependentRowset('Reviews');
Die findDependentRowset()-Methode akzeptiert zwei Parameter: den Namen der Tabellenklasse, aus denen Sie Daten beziehen wollen, und einen optionalen Regelnamen. Der Regelname muss ein Schlüssel aus der vorab erstellten $_referenceMap sein, doch wenn Sie keinen angeben, sucht er nach einer Regel mit einer refTableClass, die so ist wie der Klassenname der Tabelle, aus der die Zeile erstellt wurde (in unserem Falle „Places“). Wir wenden uns nun den many-to-many-Beziehungen zu, die mit einer zusammengeführten oder Link-Tabelle arbeiten. 6.3.2.2
Many-to-many-Beziehungen
Per Definition kann jeder Zielort mehrere Rezensionen haben, die von verschiedenen Usern geschrieben wurden. Somit können wir für jeden beliebigen User eine Liste der Zielorte ausgeben, die von ihm besprochen wurden, und wir können die User auflisten, die einen bestimmten Ort rezensiert haben. Das nennt man eine many-to-many-Beziehung zwischen den Zielorten und den Usern.
150
6.3 Zend_Db_Table als Model Um diese Beziehung in einer Datenbank zu mappen (abzubilden), ist eine dritte Tabelle erforderlich, die man als Link-Tabelle bezeichnet. In unserem Fall ist die Link-Tabelle die Tabelle reviews (siehe Abbildung 6.4). Places Reviews
+id: int
+date_updated: datetime
Users
+id: int
+date_created: datetime place_id
+id: int
+date_created: datetime
+date_created: datetime
+name: varchar(100)
+date_updated: int
+address1: varchar(100)
+place_id: int
+username: varchar(100)
+address2: varchar(100)
+user_id: int
+password: varchar(40)
+address3: varchar(100)
+body: mediumtext
+first_name: varchar(100)
+town: varchar(75)
+rating: int
+last_name: varchar(100)
+county: varchar(75)
+helpful_yes: int
+date_of_birth: date
+postcode: varchar(30)
+helpful_total: int
+sex: char(1)
user_id
+country: varchar(75)
+date_updated: datetime
+postcode: varchar(20)
Abbildung 6.4 Eine many-to-many-Beziehung zwischen places und users wird über die reviewsTabelle erstellt, die als Link agiert.
Die reviews-Tabelle hat zwei Fremdschlüssel, eine für places und eine für users, und somit haben wir es also mit zwei one-to-many-Beziehungen zu tun. Wir müssen drei Klassen definieren (siehe Listing 6.14). Listing 6.14 Eine many-to-many-Beziehung mit Zend_Db_Table class Places extends Zend_Db_Table_Abstract { protected $_name = 'places'; protected $_dependentTables = array('Reviews'); } class Reviews extends Zend_Db_Table_Abstract { protected $_name = 'reviews'; protected $_referenceMap = array( 'Place' => array( 'columns' => array('place_id'), 'refTableClass' => 'Places', 'refColumns' => array('id') ), 'User' => array( 'columns' => array('user_id'), 'refTableClass' => 'Users', 'refColumns' => array('id') ) ); } class Users extends Zend_Db_Table_Abstract { protected $_name = 'users'; protected $_dependentTables = array('Reviews'); }
Definiert zwei Regeln, um die Beziehung zu erstellen
Definiert nur die direkt abhängige Klasse
151
6 Mit der Datenbank arbeiten Wie Sie in Listing 6.14 sehen können, folgt die Klasse Users dem gleichen Muster wie die Places-Klasse und definiert nur die Tabellenklassen, von denen sie direkt abhängig ist. Das bedeutet, dass wir nicht explizit eine many-to-many-Beziehung im Code angeben, sondern Zend_Db_Table herausfinden lassen, dass es eine gibt. Um eine Liste der Orte zu erstellen, die ein bestimmter User besprochen hat, nehmen wir den folgenden Code: $users = new Users(); $robAllen = $users->fetchRow('id = 1'); $places = $robAllen->findManyToManyRowset('Places', 'Reviews');
Wie Sie sehen, heißt die Funktion, die die Arbeit für uns erledigt, findManyToManyRowset(). Der erste Parameter ist die Zielorttabellenklasse und der zweite die Schnitttabellenklasse, die die Regeln enthält, die die erste Tabellenklasse mit der Schnitttabellenklasse verlinkt. In beiden Fällen können die Parameter entweder Strings oder Instanzen von Zend_Db_Table sein. Also könnte man das vorige Snippet auch wie folgt schreiben: $users = new Users(); $places = new Places(); $reviews = new Reviews(); $robAllen = $users->fetchRow('id = 1'); $places = $robAllen->findManyToManyRowset($places, $reviews);
Das führt zum exakt gleichen Ergebnis. Beachten Sie, dass Sie angeben können, welche Regel als zusätzlicher Parameter für findManyToManyRowset() zu verwenden ist, falls es mehr als eine Regel in der $_referenceMap der Schnitttabelle gibt, die die Tabellen verlinkt. Nehmen wir die Situation, dass jede Rezension von einem Moderator freigegeben werden soll. Das neue Tabellendiagramm würde dann aussehen wie in Abbildung 6.5. Places Reviews
+id: int
+date_updated: datetime
Users
+id: int
+date_created: datetime place_id
+id: int
+date_created: datetime
+date_created: datetime
+name: varchar(100)
+date_updated: int
+address1: varchar(100)
+place_id: int
+username: varchar(100)
+address2: varchar(100)
+user_id: int
+password: varchar(40)
+address3: varchar(100)
+body: mediumtext
+first_name: varchar(100)
+town: varchar(75)
+rating: int
+county: varchar(75)
+helpful_yes: int
+postcode: varchar(30)
+helpful_total: int
+sex: char(1)
+country: varchar(75)
+approved_by: int
+postcode: varchar(20)
user_id
+date_updated: datetime
+last_name: varchar(100) approved_by
+date_of_birth: date
Abbildung 6.5 Mehrere many-to-many-Beziehungen zwischen places und users werden über zwei Schlüssel in der reviews-Tabelle erstellt.
In diesem Fall haben wir nun zwei Fremdschlüssel in der reviews-Tabelle, die mit der users-Tabelle verlinkt ist. Um den zweiten Link zu implementieren, ist in der $_referenceMap für die reviews-Tabelle eine neue Regel erforderlich (siehe Listing 6.15).
Wie Sie sehen können, können wir in der Reference-Map mehrere Regeln haben, die sich auf die gleiche Tabelle beziehen, solange sie unterschiedlich heißen (in diesem Fall User und ApprovedBy) Wir nehmen dann den columns-Schlüssel in jedem Map-Element, damit Zend_Db_Table den Feldnamen des korrekten Fremdschlüssels in der Reviews-Tabelle erfährt. Die Wahl der Liste von Zielorten, die Rezensionen haben, die von einem bestimmten User empfohlen worden sind, erfolgt wiederum über findManyToManyRowset(), doch dieses Mal übergeben wir den Namen der Regel, die wir verwenden wollen: // Finde alle Stellen, an denen John Smith eine Rezension bewertet hat $johnSmith = $users->fetchRow('id = 4'); $places = $johnSmith->findManyToManyRowset('Places', 'Reviews', 'ApprovedBy'); Zend_Db_Table erweist sich im Umgang der one-to-many- und many-to-many-Beziehungen als außerordentlich leistungsfähig, obwohl wir noch die Beziehungen in der Schnittklasse manuell einrichten müssen.
6.4
Zusammenfassung In diesem Kapitel schauten wir uns an, wie Zend Framework Datenbanken unterstützt. Weil beinahe alle Webapplikationen mit Datenbanken arbeiten, um ihre Daten zu speichern, ist es kein Wunder, dass im Zend Framework ein umfassender Datenbank-Support zu finden ist. Die Grundlage ist die Datenbankabstraktionsschicht Zend_Db_Abstract, die sich um die Unterschiede zwischen den jeweiligen Datenbank-Engines kümmert und somit ein standardisiertes Interface für den Rest der Komponenten im Framework bietet.
153
6 Mit der Datenbank arbeiten baut auf dieser Grundlage auf und stellt ein tabellenbasiertes Interface für die Daten in der Tabelle zur Verfügung. Somit wird die Arbeit mit der Datenbank im Model einer MVC-Applikation deutlich einfacher. Wenn man dann mal mit einer speziellen Zeile der Daten arbeiten will, ist der Zugriff über Zend_Db_Table_Row sehr einfach.
Zend_Db_Table
Nun haben wir die Elemente der zugrunde liegenden Struktur einer Zend FrameworkApplikation abgedeckt. Wir machen im nächsten Kapitel bei Authentifizierung und Autorisierung weiter: So beschränken wir bestimmte Bereiche unserer Applikation auf spezielle User und können sicher sein, dass diese User nur Aktionen ausführen, die der von der Applikation in sie gesetzten Vertrauensstufe entsprechen.
154
7 7
Benutzerauthentifizierung und Zugriffskontrolle
Die Themen dieses Kapitels
Grundlagen von Authentifizierung und Zugriffskontrolle Das Login auf Websites mit Zend_Auth umsetzen Mit Zend_Acl den Zugriff auf bestimmte Webseiten einschränken Auf den meisten Websites ist der Zugang zu bestimmten Bereichen für verschiedene Personen eingeschränkt. Die meisten E-Commerce-Sites erwarten beispielsweise, dass Sie eingeloggt sind, bevor Sie mit der Bestellung zur Kasse gehen können. Bei anderen Sites gibt es nur Mitgliedern vorbehaltene Seiten, die nur nach dem Einloggen aufrufbar sind. In der Places-Applikation können nur registrierte User eine Rezension schreiben oder beurteilen. Diese Funktionen bezeichnet man als Authentifizierung und Zugriffskontrolle, und in diesem Kapitel wird es darum gehen, wie Zend Framework das in Form der Komponenten Zend_Auth und Zend_Acl unterstützt. Wir beginnen mit den Grundlagen von Authentifizierung und Zugriffskontrolle und machen dann mit der Integration von Zend_Auth und Zend_Acl in Places weiter.
7.1
Benutzerauthentifizierung und Zugriffskontrolle Wenn es darum geht, einem User den Zugriff auf bestimmte Seiten einer Website zu erlauben, sind zwei unterschiedliche Prozesse beteiligt. Authentifizierung ist der Prozess der Identifizierung einer Person basierend auf deren Anmeldeinformationen (meist Username und Passwort), und mit Zugriffskontrolle ist der Prozess gemeint, wie man entscheidet, ob dem User bestimmte Handlungen erlaubt sind. Die Komponenten Zend_Auth und Zend_Acl des Zend Frameworks unterstützen umfassend alle Aspekte der Authentifizierung und Zugriffskontrolle für Websites.
155
7 Benutzerauthentifizierung und Zugriffskontrolle Weil Sie wissen müssen, um wen es sich bei einem Benutzer handelt, bevor Sie entscheiden können, was er machen darf, lässt sich schließen, dass der Authentifizierungsprozess vor der Zugriffskontrolle erfolgen muss. Also beschäftigen wir uns hier zuerst mit Zend_Auth und der Authentifizierung, bevor wir uns Zend_Acl zuwenden.
7.1.1
Was ist Authentifizierung?
Das Ziel der Authentifizierung ist zu entscheiden, ob jemand wirklich derjenige ist, für den er sich ausgibt. Es gibt drei Möglichkeiten, die sogenannten Faktoren, um einen User zu erkennen:
Etwas, was er weiß, z. B.. ein Passwort oder eine PIN Etwas, was er besitzt, z. B.. einen Führerschein oder eine Kreditkarte Etwas, das er ist, was also seiner Person eigen ist wie z. B.. ein Fingerabdruck oder ein Tippmuster. Wenn Sie mit Ihrer Kreditkarte in einem Laden etwas kaufen, können Sie die Transaktion mit zweien dieser Faktoren authentifizieren: dem Faktor Besitzen (der Kreditkarte in Ihrer Geldbörse) und dem Faktor Wissen (der PIN). Anderswo in der Welt könnte dazu noch das Wissen der Unterschrift hinzukommen. Für praktisch jede Website, die es gibt (einschließlich der Online-Banken), ist der Wissen-Faktor der einzige Mechanismus, der zur Identifikation eines Users verwendet wird. Generell sind damit ein Username und ein Passwort gemeint, obwohl Banken neben einem Passwort auch gerne mehreren Informationsaspekten fragen (z. B.. einem besonderen Datum oder einem Ort). Wir werden dem Standard im Web folgen und zur Authentifizierung Username und Passwort kombinieren, doch der Speicherplatz der Information muss noch bestimmt werden. Bei Standalone-Websites ist es üblich, eine Liste von Usernamen und Passwörtern in einer Datenbanktabelle abzulegen, aber es gibt auch andere Optionen. Bei Sites, die Teil einer Gruppe sind (wie z. B.. Yahoo!), ist ein separates System erforderlich, das die Authentifizierung regelt. Ein häufig verwendetes System ist LDAP (Lightweight Directory Access Protocol), das Informationen über die User in einem separaten Dienst speichert, der bei Bedarf von anderen Applikationen abgefragt werden kann. OpenID und TypeKey von Six Apart sind andere Systeme, mit denen eine Authentifizierung durch einen externen Dienst durchgeführt werden kann. Bei Places legen wir die Login-Details unserer User in einer Datenbanktabelle ab.
7.1.2
Was ist Zugriffskontrolle?
Mit Zugriffskontrolle ist der Prozess gemeint, wie man entscheidet, ob ein User auf eine Ressource oder Aktion zugreifen darf. Auf das Internet bezogen heißt das meistens, dass wir entscheiden, ob jemand eine bestimmte Seite aufrufen oder eine Handlung wie das Einfügen eines Kommentars durchführen darf. Ein Standardmechanismus dafür ist die Verwendung einer Zugriffskontrollliste (Access Control List, ACL), also einer Liste von
156
7.2 Die Implementierung der Authentifizierung Genehmigungen (permissions), die mit einer Ressource verbunden wird. In der Liste steht, wer auf die Ressource zugreifen und was damit gemacht werden darf. Die Liste informiert das System also, ob ein bestimmter User nur einen Datenbankeintrag anschauen oder eine Controller-Action durchführen darf. Sobald ein User etwas machen will, prüft die Applikation anhand der Liste, ob ihm erlaubt werden kann, diese Aktion mit diesem Datenelement auszuführen. Ein User darf beispielsweise einen Nachrichtenartikel lesen, ihn aber nicht bearbeiten. Nach der Definition der Begriffe schauen wir uns nun an, wie man diese Features mittels der Komponenten des Zend Frameworks implementiert. Wir beginnen bei der Authentifizierung mit Zend_Auth.
7.2
Die Implementierung der Authentifizierung Zuerst schauen wir uns an, wie man die Authentifizierung mit Zend_Auth anhand der HTTP-Authentifizierung implementiert. Dann beschäftigen wir uns damit, wie die Authentifizierung bei einer echten Applikation über eine Datenbank, in der die UserInformationen und die Sessions (um die Information über mehrere Seitenaufrufe hinweg zu speichern) enthalten sind, implementiert wird.
7.2.1
Die Komponente Zend_Auth
gehört zu dem Teil des Frameworks, der sich mit der Authentifizierung beschäftigt. Er teilt sich in eine Kernkomponente und verschiedene Authentifizierungsadapter auf. Die Adapter enthalten die eigentlichen Mechanismen zur Autorisierung der User, z. B.. anhand von HTTP mit einer Datei oder über eine Datenbanktabelle.
Zend_Auth
Die Authentifizierungsergebnisse nennt man die Identität, und die in der Identität gespeicherten Felder hängen vom Adapter ab. Eine HTTP-Authentifizierung wird beispielsweise nur den Usernamen in die Identität platzieren, doch bei einer Datenbankauthentifizierung könnte auch der vollständige Name und die E-Mail-Adresse angegeben werden. Weil es üblich ist, den Namen des eingeloggten Users darzustellen, ist dieses Feature sehr praktisch. Um an die Identitätsinformationen zu kommen, verwendet Zend_Auth das SingletonDesignpattern, um die Identitätsergebnisse bei Bedarf auszulesen. Einige der bereits im Lieferzustand enthaltenen Authentifizierungsadapter in Zend_Auth sind:
7 Benutzerauthentifizierung und Zugriffskontrolle Die Http- und Digest-Adapter autorisieren anhand einer Datei auf der Festplatte und verwenden dafür den in allen Browser eingebauten Standard-HTTP-Login-Mechanismus. Der DbTable-Adapter wird für die Autorisierung anhand einer in einer Datenbank gespeicherten User-Liste vorgenommen. Die Adapter InfoCard, Ldap und OpenId führen die Authentifizierung über Remote-Dienste durch und sind nicht Thema dieses Kapitels. Doch aufgrund der Natur des Adaptersystems von Zend_Auth ist es sehr einfach, von einem Authentifizierungsadapter zu einem anderen zu wechseln. Das System ist flexibel und so gestaltet, dass eigene Adapter leicht erstellt und in Zend_Auth integriert werden können. So kann man einfach Alt-Systeme integrieren oder die Authentifizierung mit neuen Systemen vornehmen, für die noch keine offiziellen Zend Framework-Adapter geschrieben wurden. Schauen wir uns die HTTP-Authentifizierung an.
7.2.2
Einloggen über die HTTP-Authentifizierung
Es gibt wohl nur wenige Menschen, die noch nie die von Browsern angezeigte StandardHTTP-Login-Box gesehen haben (siehe Abbildung 7.1). Wenn man dieses Login-System in der eigenen Webapplikation nutzt, hat das den Vorteil, dass es für den User vertraut ist, aber auch den Nachteil, dass es keinen einfachen Weg zum Ausloggen gibt. Außerdem können Sie das Look & Feel der Login-Box nicht anpassen.
Abbildung 7.1 Die Standard-HTTP-LoginBox eines Browsers
Um jemanden über Zend_Auth zu authentifizieren, erstellen Sie eine Instanz des authAdapters und authentifizieren anhand der authenticate()-Funktion von Zend_Auth: $authAdapter = new Zend_Auth_Adapter_Http(); // $authAdapter einrichten, damit er weiß, was er machen soll $auth = Zend_Auth::getInstance(); $result = $auth->authenticate($authAdapter);
Das HTTP-Authentifizierungsprotokoll geht davon aus, dass die Seiten, die Sie schützen wollen, in einem Bereich (realm) gruppiert sind, der für den User dargestellt wird. In Abbildung 7.1 heißt dieser Bereich beispielsweise „My Protected Area“. Der Name des Bereichs muss dem Zend_Auth_Adapter_Http-Adapter mitgeteilt werden, und Sie müssen außerdem eine Auflösungsklasse (resolver class) erstellen, um das Passwort für einen
158
7.2 Die Implementierung der Authentifizierung bestimmten Usernamen angeben zu können. Die Auflösungsklasse entkoppelt den Mechanismus für die Authentifizierung von demjenigen für das Auslesen des Usernamens und Passworts vom User, und normalerweise wird das Passwort aus einer Datenbank oder Datei ausgelesen. Abbildung 7.2 zeigt ein Flussdiagramm, das diesen Vorgang veranschaulicht. Um das zu implementieren, müssen wir zuerst Zend_Auth_Adapter_Http konfigurieren (siehe Listing 7.1).
User fordert Seite an
Aufruf von authenticate des Zend_Auth-Adapters
isValid() von Zend_Auth?
NEIN
JA
Antwortobjekt wird automatisch mit HTTP-Auth-Headern gefüllt
Erfolg! getIdentity() gibt Usernamen zurück
Absenden an Browser
User wird eine Login-Dialogbox präsentiert
Abbildung 7.2 HTTP-Authentifizierung mit den Methoden von Zend_Auth
Listing 7.1 Konfiguration von Zend_Auth_Adapter_Http // create a Zend_Auth_Adapter_Http instance $config['accept_schemes'] = 'basic'; $config['realm'] = 'ZFiA Chapter 07'; $authAdapter = new Zend_Auth_Adapter_Http($config);
Erstellt den Adapter
$resolver = new Zend_Auth_Adapter_Http_Resolver_File('passwords.txt'); $authAdapter->setBasicResolver($resolver); Richtet Zugriff auf $authAdapter->setRequest($request); $authAdapter->setResponse($response); HTTP-Header ein
Wie Sie sehen, ist die Konfiguration eines Zend_Auth_Adapter_Http-Objekts ein zweistufiger Prozess, weil manche Einstellungen im $config-Array konfiguriert werden, das bei der Konstruktion des Objekts verwendet wird n, und die Einstellungen für die resolver-, request- und response-Objekte werden nach der Erstellung vorgenommen. Das requestObjekt wird zum Auslesen von Username und Passwort verwendet, und mit dem response-Objekt werden die korrekten HTTP-Header gesetzt, mit denen eine erfolgreiche oder fehlgeschlagene Authentifizierung angezeigt wird. Die authenticate()-Funktion führt die eigentliche Authentifizierung durch (siehe Listing 7.2).
159
7 Benutzerauthentifizierung und Zugriffskontrolle Listing 7.2 Authentifizierung mit Zend_Auth_Adapter_Http
Authentifiziert angegebenen
$result = $authAdapter->authenticate(); Usernamen und Passwort if (!$result->isValid()) { // Failed to validate. The response contains the correct HTTP // headers for the browser to ask for a username/password. $response->appendBody('Sorry, you are not authorized'); } else { Liest Username und // Successfully validated. Bereich aus Resultat aus $identity = $result->getIdentity(); $username = $identity['username']; $response->appendBody('Welcome, '.$username); }
Wenn die Funktion authenticate() aufgerufen wird, sucht sie nach den HTTP-Headern für Username und Passwort n. Wenn diese nicht vorhanden sind, schlägt die Validierung fehl, und der WWW-Authenticate-Header wird im response-Objekt gesetzt. Dann stellt der Browser eine Dialogbox dar, in der zur Eingabe von Username und Passwort aufgefordert wird. Bei erfolgreicher Validierung kann der Username der Person, die sich gerade eingeloggt hat, über die getIdentity()-Funktion ausgelesen und somit die Identität zurückgegeben werden o. Der gesamte Quellcode dieses Mini-Beispiels steht in der ZipDatei auf der dieses Buch begleitenden Homepage. Wie Sie sehen, ist die HTTP-Authentifizierung mit Zend_Auth_Adapter_Http sehr einfach. Doch die meisten Websites arbeiten nicht damit. Jede öffentlich verfügbare, kommerzielle Website verwendet ein eigenes Login-Formular, um den User zu identifizieren, wobei meist nach einem Usernamen und einem Passwort gefragt wird. Das hat verschiedene Gründe:
Es gibt keine andere Möglichkeit, sich auszuloggen, als den Browser ganz zu beenden. Es ist nicht optional, d.h. Sie können die Seite für eingeloggte User nicht mit einem anderen Inhalt darstellen.
Sie können das Look & Feel der Login-Dialogbox nicht ändern oder darin zusätzliche Infos anbieten, wenn Sie beispielsweise lieber mit einer E-Mail-Adresse als einem Usernamen arbeiten wollen.
Es ist nicht klar, was ein User machen soll, falls er sein Passwort vergessen oder sich noch kein Konto registriert hat. Manche dieser Probleme kann man anhand von Cookies und JavaScript umgehen, doch die User Experience ist immer noch nicht gut genug, und darum arbeiten die meisten Sites mit Formularen und Sessions (oder Cookies). Wir werden nun die Authentifizierung mittels Login-Formular und Zend_Auth in die Places-Applikation integrieren.
160
7.3 Zend_Auth in einer echten Applikation
7.3
Zend_Auth in einer echten Applikation Weil Places eine Community-Website ist, müssen wir die Mitglieder identifizieren können, und dazu setzen wir eine User-Tabelle ein. Um die Authentifizierung zu implementieren, brauchen wir eine Controller-Action, um die Darstellung und Verarbeitung eines Formulars zu bewältigen, in dem der User seinen Usernamen und das Passwort eingeben kann. Wir müssen auch darauf achten, dass wir die Identität des aktuellen Users in der ganzen Applikation kennen, und somit nutzen wir das Front-Controller-Plug-in-System von Zend Framework.
7.3.1
Das Einloggen
Um sich bei einer Applikation ein- und auszuloggen, ist ein separater Controller namens AuthController erforderlich. Wir stellen das Formular (auth/login) über eine Action dar und lassen eine separate Action (auth/identify) die eigentliche Identifikation ausführen. Das Grundgerüst der Klasse wird in controllers/AuthController.php gespeichert und sieht so aus: class AuthController extends Zend_Controller_Action { public function indexAction() { $this->_forward('login'); } public function loginAction() { } public function identifyAction() { } }
Beachten Sie, dass wir indexAction so einrichten, dass die loginAction weitergeleitet wird, damit der /auth-URL wie erwartet funktioniert. Das heißt, wir leiten einfach auf auth/login um, falls jemand nach auth/ oder auth/index geht. Das HTML für ein Login-Formular ist sehr einfach und kommt in das View-Skript für die Login-Action (scripts/auth/login.phtml) – siehe Listing 7.3. Listing 7.3 Das Login-Formular: auth/login.phtml
Log in
Please log in here
Das HTML in Listing 7.3 ist deshalb sehr einfach, weil der User nur in zwei Feldern Informationen eingeben muss: username und password. Wie Sie sehen, nehmen wir die integrierte View-Hilfsklasse url()n, um den korrekten URL zur auth/identifyController-Action zu erstellen. Das View-Skript an sich ist an solchen Details nicht interessiert, und darum ist eine View-Hilfsklasse die ideale Lösung. In Kapitel 8 (nach der Einführung von Zend_Form) werden wir dieses Formular refakturieren, damit es mit der Zend_Form-Komponente arbeitet, anstatt von Hand programmiert zu werden. Die identifyAction()-Methode (siehe Listing 7.4) kümmert sich um den Prozess des Einloggens. Sie arbeitet mit Zend_Auth, um zu prüfen, dass der angegebene Username und das Passwort valide sind. Listing 7.4 Username und Passwort werden über identifyAction() validiert. public function identifyAction() { if ($this->getRequest()->isPost()) { $formData = $this->_getFormData();
Sucht nach POST Liest Formulardaten aus
if (empty($formData['username']) Speichert || empty($formData['password'])) { Fehlermeldung $this->_flashMessage('Empty username or password.'); } else { Richtet Daten// do the authentication bankadapter ein $authAdapter = $this->_getAuthAdapter($formData); $auth = Zend_Auth::getInstance(); $result = $auth->authenticate($authAdapter); Prüft if (!$result->isValid()) { Anmeldeinformationen $this->_flashMessage('Login failed'); } else { $data = $authAdapter->getResultRowObject(null,'password'); $auth->getStorage()->write($data);
protected function _flashMessage($message) { $flashMessenger = $this->_helper->FlashMessenger; $flashMessenger->setNamespace('actionErrors'); $flashMessenger->addMessage($message); }
162
Speichert Nachricht in FlashMessenger
7.3 Zend_Auth in einer echten Applikation Wir sind an dieser Seitenanforderung nur interessiert, wenn sie ein POST n ist. Das ist eine kleinere Sicherheitsverbesserung des Codes, weil er verhindert, dass der User mit einer GET-Anfrage arbeitet. Bei einer GET-Anfrage werden Username und Passwort in der Adresszeile dargestellt und könnten als Lesezeichen abgelegt werden – für die Sicherheit nicht gut! Wir sammeln über die Hilfsmethode _getFormData() die Daten aus der Anfrage und legen sie in ein Array o. Die Hilfsmethode filtert die Daten, damit sie auch wirklich sicher verwendet werden können. In Kapitel 8 refakturieren wir dieses Formular, damit es mit Zend_Form arbeitet, das dann das Filtern und Validieren für uns erledigt. Wenn ein Fehler auftaucht, geben wir die Nachricht über die Action-Hilfsklasse FlashMessenger an die auth/login-Action zurück p. Weil das zweimal gemacht werden muss, setzen wir die separate Hilfsmethode _flashMessage() ein. Die Einrichtung der Instanz Zend_Auth_Adapter_DbTable ist kompliziert genug, um sie in eine eigene Methode namens _getAuthAdapter() unterbringen zu können q. Die authenticate()-Nachricht erledigt die Authentifizierung und gibt ein Resultatobjekt r zurück. Die Methode isValid() des Resultats wird für den Test auf ein erfolgreiches Login verwendet. Wenn die Authentifizierung erfolgreich ist, speichern wir den Datenbankeintrag (ohne das Passwortfeld) in der Session s. Nach erfolgreicher Authentifizierung leiten wir dorthin um, wo der User hin möchte t. Beim Fehlschlagen der Authentifizierung wird er zurück zum Login-Formular geleitet u. Die _flashMessage()-Methode übergibt die angegebene Nachricht an die nächste Anfrage, also das Login-Formular v. Die Action-Hilfsklasse FlashMessenger speichert eine einmalige Nachricht, die sich nach dem Lesen automatisch selbst löscht. So kann man Validierungsnachrichten ideal von einem Bildschirm zum andern schicken. In unserem Fall wird es von der Controller-Action loginAction() gelesen, und die Nachricht wird über den folgenden Code der View zugewiesen: $flashMessenger = $this->_helper->FlashMessenger; $flashMessenger->setNamespace('actionErrors'); $this->view->actionErrors = $flashMessenger->getMessages();
Listing 7.5 zeigt die Methode _getAuthAdapter(), die den letzten Teil des Authentifizierungspuzzles darstellt. Sie erstellt eine Instanz von Zend_Auth_Adapter_DbTable und weist ihr den angegebenen Usernamen und das Passwort zu, damit es gleich von Zend_Auth authentifiziert werden kann.
163
7 Benutzerauthentifizierung und Zugriffskontrolle Listing 7.5 _getAuthAdapter() richtet den Authentifizierungsadapter ein. protected function _getAuthAdapter($formData) { $dbAdapter = Zend_Registry::get('db');
Liest Datenbankadapter aus Registry
$authAdapter = new Zend_Auth_Adapter_DbTable($dbAdapter); $authAdapter->setTableName('users') Richtet ->setIdentityColumn('username') datenbankspezifische ->setCredentialColumn('password') Informationen ein ->setCredentialTreatment('SHA1(?)');
// get "salt" for better security $config = Zend_Registry::get('config'); $salt = $config->auth->salt; $password = $salt.$formData['password'];
Dafür, dass es nur eine kurze Methode ist, passiert eine ganze Menge! Das Zend_Auth_ braucht eine Verbindung zur Datenbank; zum Glück haben wir während der Bootstrap-Startup-Phase eine in der Registry gespeichert, die für diese Art Situation vorbereitet ist n. Nach der Erstellung müssen wir dem Adapter den Namen der zu verwendenden Datenbanktabelle mitteilen und welche Felder in dieser Tabelle die Identität und die Anmeldeinformationen enthalten o. In unserem Fall brauchen wir den Usernamen- und das Passwortfeld aus der Tabelle users.
Adapter_DbTable-Objekt
Zwar können Sie das Passwort in der Datenbank auch in einem reinen Textformat speichern, doch sicherer ist es, einen Hash des Passworts zu speichern. Einen Hash kann man sich als eine Art Einwegverschlüsselung vorstellen, die für einen bestimmten Quellstring einmalig ist; selbst wenn Sie den Hash kennen, können Sie immer noch nicht den ursprünglichen String bestimmen. Dabei handelt es sich um eine übliche Methode zum Speichern von Passwortdaten. Also entstanden Websites, die Tausende von Hashes für die beiden weit verbreiteten Hash-Algorithmen (MD5 und SHA1) enthalten. Um ein Reverse Engineering zu verhindern, falls unsere Daten in die falschen Hände geraden, schützen wir die Passwörter unserer User mit einem Salt p. Der Salt (engl. für Salz, eine zufällig gewählte Bitfolge) ist ein privater String. Er ist nur der Applikation bekannt und wird dem Passwort des Users vorangestellt, um ein Wort zu produzieren, das es in keinem Online-Wörterbuch gibt. Somit wird es sehr schwer, wenn nicht gar unmöglich, den Hash per Reverse Engineering zu knacken. Wir nutzen die Datenbank, um das eigentliche Hashing des „gesalzenen“ Passworts durch Aufruf der Funktion setCredentialTreatment() des auth-Adapters durchzuführen, wenn wir die Datenbankdetails für $authAdapter einrichten. q Weil wir mit MySQL arbeiten, haben wir festgelegt, dass die SHA1()-Funktion wie folgt eingesetzt wird: setCredentialTreatment('SHA1(?)');
Das ? ist ein Platzhalter für den anhand von setCredential() gesetzten Passwort-String. Für andere Datenbankserver sollte die passende Funktion eingesetzt werden.
164
7.3 Zend_Auth in einer echten Applikation Wir haben nun den Login-Abschnitt unserer Applikation abgeschlossen, doch wäre es auch ganz praktisch, wenn es Links gäbe, um dem User beim Ein- und Ausloggen zu helfen. Das kann z. B.. über eine View-Hilfsklasse passieren, und sie kapselt die Logik von den View-Templates selbst ab.
7.3.2
Eine Begrüßungsnachricht in der View-Hilfsklasse
Um in einer View-Hilfsklasse eine Begrüßungsnachricht einzubauen, können wir die Tatsache nutzen, dass Zend_Auth das Singleton-Pattern implementiert. So brauchen wir keine Instanz von Zend_Auth vom Controller an die View zu übergeben. In Listing 7.6 sehen Sie die View-Hilfsklasse LoggedInUser. Listing 7.6 Die View-Hilfsklasse LoggedInUser class Zend_View_Helper_LoggedInUser { protected $view; function setView($view) { $this->view = $view; }
Die setView()-Methode wird automatisch von der View aufgerufen, bevor die Hauptfunktion der Hilfsklasse aufgerufen wird n. Damit speichern wir eine lokale Referenz auf das View-Objekt, damit wir andere View-Hilfsklassen wie url()q und escape() referenzieren können.
165
7 Benutzerauthentifizierung und Zugriffskontrolle Die eigentliche Arbeit erledigt die Methode loggedInUser(). Mit der Methode hasIdentity() von Zend_Auth wird geprüft, ob es einen eingeloggten User gibt o. Ist das der Fall, wird dessen Eintrag über die Methode getIdentity() ausgelesen p. Weil der User sich einen eigenen Usernamen aussuchen kann, nehmen wir die Methode escape(), um sicher zu sein, dass wir nicht unbeabsichtigt eine XSS-Schwachstelle einbauen, und erstellen dann den darzustellenden String. Wenn der aktuelle Besucher nicht eingeloggt ist, wird ein String mit einem Link erstellt, über den sich der User einloggen kann r. Wir rufen diese View-Hilfsklasse in unserem Haupt-Layout-Skript auf, um den Link in der Hauptmenüleiste durch diesen Code darzustellen:
loggedInUser(); ?>
Das finale Ergebnis sieht aus wie in Abbildung 7.3 und enthält einen leicht zu findenden Logout-Link. Nun können wir die Möglichkeit zum Ausloggen implementieren.
Abbildung 7.3 Durch die Login-Begrüßungsnachricht auf der Website bekommt der User eine Rückmeldung, ob er eingeloggt ist.
7.3.3
Das Ausloggen
Im Vergleich zum Einloggen ist das Ausloggen sehr einfach. Wir brauchen nur die Funktion clearIdentity() von Zend_Auth aufzurufen. Listing 7.7 zeigt die Logout-Action in der AuthController-Klasse. Listing 7.7 Die auth/logout-Controller-Action public function logoutAction() { $auth = Zend_Auth::getInstance(); $auth->clearIdentity(); $this->_redirect('/'); }
166
Setzt SessionVariable zurück
Leitet auf Homepage zurück
7.4 Die Implementierung der Zugriffskontrolle Wie unschwer zu erkennen, ist das Logout recht simpel. Nun wird es Zeit, mit der Zugriffskontrolle weiterzumachen und eingeloggten Usern mehr Rechte als Besuchern zu geben. Darum kümmert sich Zend_Acl, ein Verwandter von Zend_Auth.
7.4
Die Implementierung der Zugriffskontrolle Wie bereits in Abschnitt 7.1 angesprochen, wird mit der Zugriffskontrolle der Prozess bezeichnet, dass ein eingeloggter User auf eine bestimmte Ressource zugreifen darf. Das kann man auf vielerlei Weise machen, doch eine flexible und standardisierte Methode ist der Einsatz rollenbasierter Zugriffskontrolllisten (Access Control Lists, ACL). Für uns kümmert sich Zend_Acl im Zend Framework darum. Fachbegriffe bei der Zugriffskontrolle Es gibt eine Menge Jargon beim Thema Zugriffskontrolle. Dies hier sind die Schlüsselbegriffe: Rolle: eine Gruppierung von Benutzern Ressource: etwas Schützenswertes wie eine Controller-Action oder ein Dateneintrag Privileg: die Art des erforderlichen Zugriffs wie Lesen (read) oder Bearbeiten (edit). ACLs sind eine sehr flexible Lösung zur Steuerung des Zugriffs. Sie brauchen nur zu kennzeichnen, welche Elemente in Ihrer Applikation sich auf die Rolle, die Ressource und das Privileg beziehen. In Abbildung 7.4 wird gezeigt, wie die drei Hauptteile des Puzzles miteinander in Beziehung stehen.
Privileg: darf zugreifen
Resource: NewsController::viewAction()
Privileg: darf zugreifen
Privileg: darf nicht zugreifen
Resource: NewsController::editAction()
Privileg: darf zugreifen
Rolle: Mitglied
Rolle: Bearbeiter
Abbildung 7.4 Die Beziehung zwischen den Teilen des ACL-Puzzles
Dies ist nur eine Möglichkeit, wie man mit ACLs den Zugriff auf eine Ressource regeln kann (in diesem Fall die Controller-Actions). Wir schauen uns zuerst Zend_Acl an und danach, wie man ACL nutzt, um den Zugriff auf verschiedene Controller-Actions zu steuern. Schließlich beschäftigen wir uns mit der Zugriffskontrolle auf Datenbankebene.
167
7 Benutzerauthentifizierung und Zugriffskontrolle
7.4.1
Die Arbeit mit Zend_Acl
kann recht einfach benutzt werden, weil die API klar ist. Sie können einige Rollen und Ressourcen erstellen, die erforderlichen Genehmigungen einrichten und die Funktion isAllowed() aufrufen. Hört sich doch leicht an! Schauen wir uns das noch etwas detaillierter an und beginnen dafür bei Zend_Acl_Role.
Zend_Acl
7.4.1.1
Zend_Acl_Role
Eine Rolle ist eine Verantwortlichkeit oder Zuständigkeit, die ein User in einem System hat. Eine typische Rolle wäre „Nachrichtenbearbeiter“ oder „Mitglied“. Das ist in Zend_Acl_Role gekapselt, einer sehr einfachen Klasse, die nur den Namen der Rolle enthält. Dies ist der Code, um eine Rolle zu erstellen: $roleMember = new Zend_Acl_Role('member');
Nach Erstellung einer Rolle kann deren Name nicht mehr geändert werden. Beachten Sie außerdem, dass jeder Rollenname im Kontext von ACLs einmalig sein muss. Wir können der Zend_Acl-Instanz eine Rolle durch den folgenden Code zuweisen: $acl = new Zend_Acl(); $acl->addRole($roleMember);
Eine bestimmte Rolle kann ein übergeordnetes Element haben, was bedeutet, dass die neue Rolle alles machen kann, was die Elternrolle auch darf. Eine typische Situation wäre die Rolle eines Forum-Moderators, die man wie folgt programmieren würde: $acl = new Zend_Acl(); $acl->addRole(new Zend_Acl_Role('member')); $acl->addRole(new Zend_Acl_Role('moderator'), 'member');
In dieser Situation hat die moderator-Rolle eine übergeordnete Rolle namens member, und so kann ein moderator alles machen, was auch ein member darf – neben etwaigen anderen Dingen, die einem moderator erlaubt sind. Damit eine Rolle auch sinnvoll ist, brauchen wir eine Ressource, die es zu schützen gilt. Das fällt in die Verantwortung von Zend_Acl_Resource. 7.4.1.2
Zend_Acl_Resource
Eine Ressource ist etwas, was Sie schützen wollen. Eine typische Ressource in einer Zend Framework-Applikation könnte ein Controller oder eine Action sein. Sie können beispielsweise den Zugriff auf den forums-Controller schützen, damit nur jene User, die zur Rolle member gehören, auf die darin enthaltenen Actions zugreifen können. Zend_Acl_Resource ist so simpel wie Zend_Acl_Role und enthält nur den Namen der Ressource. Sie wird wie folgt erstellt: $forumResource = new Zend_Acl_Resource('comments');
168
7.4 Die Implementierung der Zugriffskontrolle Eine Ressource wird wie eine Rolle über die add()-Methode an Zend_Acl angehängt. Auch hier werden übergeordnete Elemente unterstützt: $acl = new Zend_Acl(); $acl->addRole(new Zend_Acl_Role('member')); $acl->addRole(new Zend_Acl_Role('moderator'), 'member'); $acl->add(new Zend_Acl_Resource('forum')); $acl->add(new Zend_Acl_Resource('posts'), 'forum'); $acl->add(new Zend_Acl_Resource('threads'), 'forum');
Beachten Sie, dass add() die Methode zum Hinzufügen einer Ressource ist, und dass wir keinen String, sondern eine Instanz von Zend_Acl_Resource übergeben. In diesem Beispiel haben wir Ressourcen namens posts und threads erstellt, die untergeordnete Elemente der forum-Ressource sind. Wenn wir Ressourcen auf Module und Controller mappen, repräsentieren die Ressourcen posts und threads die Controller im Modul forum. Um Ressourcen und Rollen zu verlinken, müssen wir Genehmigungen einrichten. Das wird direkt im Zend_Acl-Objekt erledigt. 7.4.1.3
Einrichten der Genehmigungen von Zend_Acl
Der abschließende Teil der Einrichtung eines Zend_Acl-Objekts besteht darin, dem Objekt mitzuteilen, welche Genehmigungen eine bestimmte Rolle für den Zugriff auf eine bestimmte Ressource hat. Dafür müssen wir das Konzept der Privilegien hinzuziehen. Ein Privileg ist die Art des erforderlichen Zugriffs. Normalerweise basieren Privilegien auf den Operationen, die ausgeführt werden, und haben somit Namen wie „view“, „create“, „update“ usw. Bei Zend_Acl können Genehmigungen anhand der Methoden allow() und deny() eingerichtet werden. Wir beginnen damit, dass allen Rollen der Zugriff auf alle Ressourcen untersagt ist. Die Methode allow() erlaubt dann einer Rolle, auf eine Ressource zuzugreifen, und deny() entfernt eine Untermenge des erlaubten Zugriffs für eine bestimmte Situation. Auch Vererbung kommt hier ins Spiel, weil Genehmigungen, die für eine Elternrolle gesetzt werden, auch zu den Kindrollen kaskadieren. Schauen wir uns ein Beispiel an: $acl->allow('member', 'forum', 'view'); $acl->allow('moderator', 'forum', array('moderate', 'blacklist');
Hier haben wir der member-Rolle view-Privilegien für die Ressource forum gegeben und der moderator-Rolle die zusätzlichen Privilegien moderate und blacklist, die auch viewPrivilegien hat, weil moderator ein Kind von member ist. Nun kennen wir die Grundbausteine von Zend_Acl und die Schlüsselkomponenten seiner API. Als Nächstes schauen wir uns an, wie man das in eine Zend Framework-Applikation wie Places integriert. Zuerst wählen wir den Ansatz, bestimmte Controller und Actions für spezielle Rollen zu schützen.
169
7 Benutzerauthentifizierung und Zugriffskontrolle
7.4.2
Die Konfiguration eines Zend_Acl-Objekts
Wie bereits angemerkt, ist ein nicht unwesentlicher Konfigurationsaufwand erforderlich, bevor wir das Zend_Acl-Objekt nutzen können. Das bedeutet, dass die Einrichtung so einfach wie möglich sein sollte, und das bekommen wir hin, indem wir die Rollen in unserer Datei config.ini speichern. Für jede Rolle müssen wir deren Namen und Elternelement in der INI-Datei speichern. Dann können wir sie in die INI-Datei eingeben (siehe Listing 7.8). Listing 7.8 Einträge in der INI-Datei zur Konfiguration von Zend_Acl-Rollen acl.roles.guest = null acl.roles.member = guest acl.roles.admin = member
Setzt Elternelement von guest auf null
Um die Konfigurationsdatei zu lesen, erweitern wir Zend_Acl, damit es die Rollen eines Zend_Config-Objekts auslesen und bei sich einlesen kann. Der Code dafür steht in einer Klasse namens Places_Acl, die in library/Places/Acl.php gespeichert wird (siehe Listing 7.9). Listing 7.9 Das erweiterte Zend_Acl-Objekt class Places_Acl extends Zend_Acl { public function __construct() Liest config-Objekt { aus Registry $config = Zend_Registry::get('config'); $roles = $config->acl->roles; $this->_addRoles($roles); } protected function _addRoles($roles) Iteriert durch { alle Rollen foreach ($roles as $name=>$parents) { if (!$this->hasRole($name)) { Fügt Rolle hinzu, falls if (empty($parents)) { sie noch nicht existiert $parents = null; } else { $parents = explode(',', $parents); } $this->addRole(new Zend_Acl_Role($name), $parents); } } } }
Wir haben Zend_Config bereits untersucht, und von daher sollte es nicht überraschend sein, wie einfach die Daten aus dem config-Objekt über eine foreach()-Schleife ausgelesen werden können. Bei jeder Rolle müssen wir prüfen, ob sie nicht vielleicht schon hinzugefügt wurde. Dann erstellen wir eine neue Zend_Acl_Role und fügen sie in dieses Objekt ein. Über das Acl-Objekt werden die Genehmigungen für jede Action geprüft, also können wir es im Bootstrap erstellen.
170
7.4 Die Implementierung der Zugriffskontrolle
7.4.3
Das Zend_Acl-Objekt prüfen
Wir müssen darauf achten, dass wir das Acl-Objekt nicht mit Informationen über jeden Controller und jede Action beladen. Die Hauptprobleme sind Wartbarkeit und Performance. Wenn man die Liste der Controller unabhängig von den eigentlichen ControllerKlassen-Dateien pflegt, führt das zu Ungenauigkeiten. Wir wollen auch weder Zeit noch Speicherplatz damit verschwenden, die ACLs mit Regeln zu befrachten, die nie erforderlich sind, weil das zu einer sinnlosen Performance-Vergeudung führt. Unter Berücksichtigung dieser Anforderungen kann man die ACL-Prüfung gut als ActionHilfsklasse implementieren. Eine Action-Hilfsklasse geht auf Controller-Ebene in das MVC-Dispatch-System und erlaubt uns, die ACL-Regeln zu prüfen, bevor die preDispatch()-Methode des Controllers aufgerufen wird. Das geschieht vor Ausführung der Action und ist somit ein idealer Ort für den Check. Wir müssen das Acl-Objekt mit den Regeln befüllen, die anhand von preDispatch() geprüft werden sollen. Weil das schon früher im Prozess erledigt werden soll, ist die init()Funktion genau der richtige Ort. In diesem Fall sind die Regeln spezifisch für den Controller, und sie werden somit bei Bedarf für jeden Controller geschrieben. Abbildung 7.5 zeigt den kompletten Ablauf von der Zugriffsanfrage über die Action eines Controllers bis zum erfolgten Zugriff. Der Action-Controller heißt Places_Controller_Action_Helper_Acl und kümmert sich um zwei wichtige Funktionen: Er stellt dem zugrunde liegenden Acl-Objekt ein controllerzentriertes Interface zur Verfügung und führt außerdem die Authentifizierung durch. Der Controller ist zu lang, als dass er hier komplett abgedruckt werden könnte, und somit schauen wir uns nur den Teil an, der die eigentliche Arbeit durchführt (vollständig finden Sie diese Klasse im begleitenden Quellcode).
User versucht, auf Controller-Action zuzugreifen
Front-Controller-Plug-in Über Zend_Auth wird die Rolle des Users gesucht. Ist er nicht eingeloggt, bekommt er standardmäßig die Rolle "guest".
Controller init() Zend_Acl-Regeln für diesen Controller einrichten
predispatch() der Action-Hilfsklasse Steht in Zend_Acl geschrieben, dass diese Rolle Zugriff auf die Action hat?
NEIN
JA
Action-Hilfsklasse nutzt Zend_Auth
Erfolg! Zugriff auf Controller-Action gewährt
Umleitung auf Login-Seite
loginAction() JA
Dem User wird die Login-Dialogbox präsentiert
identifyAction() Ist die Authentifizierung mit Zend_Auth erfolgreich?
NEIN
Abbildung 7.5 Zend_Auth und Zend_Acl arbeiten für den Zugriff auf eine Controller-Action zusammen.
171
7 Benutzerauthentifizierung und Zugriffskontrolle Listing 7.10 zeigt das Grundgerüst der Klasse und das anfängliche Setup. Listing 7.10 Einrichten der ACL-Action-Hilfsklasse class Places_Controller_Action_Helper_Acl extends Zend_Controller_Action_Helper_Abstract { protected $_action; protected $_auth; protected $_acl; protected $_controllerName; public function __construct(Zend_View_Interface $view = null, array $options = array()) { $this->_auth = Zend_Auth::getInstance(); Speichert auth$this->_acl = $options['acl']; und acl-Objekte in } Elementvariablen
/** * Hook into action controller initialization * @return void */ public function init() { $this->_action = $this->getActionController(); // add resource for this controller $controller = $this->_action->getRequest() ->getControllerName(); if(!$this->_acl->has($controller)) { $this->_acl->add( new Zend_Acl_Resource($controller)); }
Benennt
Ressource nach dem Controller
} }
Über den Konstruktor wird anhand des options-Arrays eine Instanz des Acl-Objekts übergeben, mit der wir arbeiten werden n. Wir müssen das options-Array verwenden, weil die Signatur des Konstruktors bereits in der Elternklasse Zend_Controller_Action_Helper_ Abstract gesetzt ist. Weil die Klasse ein Singleton ist, können wir deren getInstance()Funktion nutzen, um eine Referenz darauf zu bekommen. Die init()-Methode einer Action-Hilfsklasse wird vor dem Aufruf der init()-Methode des Controllers aufgerufen. Somit ist das eine ideale Stelle, um die erforderlichen Ressourcen für den Controller einzurichten o. Wir nehmen den Namen des Controllers für die Ressource, weil dieser in der Places-Applikation einmalig ist. Mehr Ressourcen brauchen wir nicht. Wir werden die Privilegien von Zend_Acl als Actions nehmen. Also haben wir hier keine Performance-Einbußen durch das dynamische Herausfinden der Namen der Action-Methoden, um sie als Ressourcen einfügen zu können. Listing 7.11 zeigt die controller-zentrierten Regelmethoden allow() und deny(), die einen Proxy zum zugrunde liegenden Acl-Objekt aufbauen.
172
7.4 Die Implementierung der Zugriffskontrolle Listing 7.11 Die Regelmethoden der ACL-Action-Hilfsklasse public function allow($roles = null, $actions = null) { $resource = $this->_controllerName; $this->_acl->allow($roles, $resource, $actions); return $this; }
Setzt Ressourcenname auf Controller-Name
Ruft Acl-Funktion auf
public function deny($roles = null, $actions = null) { $resource = $this->_controllerName; $this->_acl->deny($roles, $resource, $actions); return $this; }
Mit den Funktionen allow() und deny() werden die Authentifizierungsregeln für den Controller eingerichtet. Dafür sorgt normalerweise die init()-Methode des Controllers. Die Versionen allow() und deny() der View-Hilfsklasse befüllen einfach für uns die Ressourcenparameter n und ändern die Terminologie von Privilegien zu Actions. Diese rufen dann die entsprechende Zend_Acl-Methode auf o. Obwohl das scheinbar nicht sonderlich viel ist, wird deutlich, wie sinnvoll es ist, wenn man die Regeln im Controller erstellt, denn dann wird auch die Wartung viel einfacher. Der letzte Teil der Action-Hilfsklasse ist die Hook-Methode preDispatch(). Diese wird automatisch vom Front-Controller aufgerufen und ist der Ort, wo wir prüfen, ob der User auch ausreichende Rechte zum Fortfahren hat. Diese Methode steht in Listing 7.12. Listing 7.12 Die preDispatch()-Methode der ACL-Action-Hilfsklasse
public function preDispatch() Setzt Standardrolle auf guest { Prüft, ob User $role = 'guest'; eingeloggt ist if ($this->_auth->hasIdentity()) { $user = $this->_auth->getIdentity(); if(is_object($user)) { Liest $role = User-Rolle aus $this->_auth->getIdentity()->role; } }
if (!$this->_acl->has($resource)) { $resource = null; }
Prüft, ob $resource valide ist
if (!$this->_acl->isAllowed($role, $resource, $privilege)) {
Testet, ob Rolle
weitermachen darf
173
7 Benutzerauthentifizierung und Zugriffskontrolle $request->setModuleName('default'); $request->setControllerName('auth'); $request->setActionName('login'); $request->setDispatched(false); }
Setzt Anfrage zurück,
damit sie auf Login-Action zeigt
}
Zuerst setzen wir die Rolle auf guest n und prüfen, ob der User eingeloggt ist o. Wenn der User eingeloggt ist, lesen wir die zugewiesene Rolle aus p. Beachten Sie, dass wir dafür die users-Tabelle aktualisieren müssen: Sie soll eine String-Spalte namens role enthalten, in der ein Wert aus der Liste der verfügbaren Rollen steht, die in der Datei config.ini in Listing 7.8 eingerichtet wurde. Wenn die korrekte, zu testende Rolle bestimmt wurde, suchen wir in der Anfrage nach der Ressource und dem Privileg q. Die Ressource ist der Controller-Name, und das Privileg ist der Action-Name. Dann wird geprüft, ob bei der Ressource ACL-Regeln gelten, und falls das nicht der Fall ist, setzen wir das auf null r, damit der isAllowed()-Check keine Exception wirft. Schließlich rufen wir isAllowed() auf, um herauszufinden, ob diese Rolle auf diese Controller-Action zugreifen darf s. Falls nicht, ändern wir die Einstellungen der Anfrage, damit der Front-Controller zur Login-Action des auth-Controllers dispatcht t und somit das Login-Formular darstellt. Um die Ressource zu testen, muss sie eingerichtet werden, bevor preDispatch() aufgerufen wird. Wir richten die in der init()-Methode erforderlichen Ressourcen und Privilegien ein, weil das vor preDispatch() geschieht. Wie man erwarten sollte, haben wir abhängig vom Controller in Places unterschiedliche Regeln. Beim IndexController soll jeder zugreifen können, also ist die init()-Methode einfach wie folgt: class IndexController extends Zend_Controller_Action { public function init() { $this->_helper->acl->allow(null); } //... Klasse wird fortgesetzt ...
Bei anderen Controllern werden die Regeln komplexer sein. Listing 7.13 zeigt beispielsweise, was im PlaceController nötig ist, damit die Mitglieder Einträge nur anschauen, aber nicht verwalten können.
174
7.5 Zusammenfassung Listing 7.13 Komplexere ACL-Regeln, damit nur Admins die Einträge für die Zielorte verwalten können class PlaceController extends Zend_Controller_Action Richtet ACL-Regeln { für member ein function init() { $memberActions = array('index', 'details', 'report-error'); $this->_helper->_acl->allow('member', $memberActions); $adminActions = array('add', 'edit', 'delete'); $this->_helper->_acl->allow('admin', $adminActions); } //... class continues...
Richtet ACL für admin ein
Wie Sie sehen, darf die member-Rolle auf den einen Set Actions n zugreifen und die Administratoren den anderen Set o. Wir brauchen dem System nicht explizit zu sagen, dass die Administratoren auf die member-Actions zugreifen dürfen, weil die admin-Rolle ein Kind der member-Rolle ist und somit deren Genehmigungen automatisch erbt. Damit schließen wir das Thema der Integration von Zend_Auth und Zend_Acl in der Places-Applikation ab. Wir haben den Usern durch Zend_Auth erlaubt, sich auf der Website einzuloggen, und durch Zend_Acl steuern wir genau, auf welche Controller und Actions ein bestimmter User zugreifen darf. Das erreichen wir, indem jeder Controller als AclRessource gesetzt wird und die Action als Privileg bekommt.
7.5
Zusammenfassung In diesem Kapitel beschäftigten wir uns mit den beiden zusammenhängenden Konzepten der Authentifizierung und der Zugriffskontrolle. Durch Authentifizierung kennen wir die Identität des aktuellen Users, und Zend_Auth ist eine intuitive und umfassende Komponente, mit der man solche Prüfungen anhand verschiedener Datenquellen vornehmen kann. Durch Nutzung von Zend_Session enthält Zend_Auth eine Allroundlösung, mit der wir den aktuell eingeloggten User sehr leicht identifizieren können. Die Wahl einer Authentifizierungsstrategie, damit nur User mit den korrekten Privilegien auf bestimmte Bereiche der Applikation zugreifen können, ist für sich genommen schon eine Kunst. Dafür gibt es im Zend Framework Zend_Acl. Wir untersuchten eine Lösung, bei der über eine Action-Hilfsklasse der Zugriff auf Controller-Actions leicht eingeschränkt werden kann, ohne dass wir eine Menge Setup-Arbeiten durchführen mussten, die von dem zu schützenden Controller unabhängig sind. Ein zentraler Vorteil der Flexibilität des Zend Frameworks ist, dass es keinen Bedarf daran gibt, die Anforderungen einer bestimmten Applikation in eine Methode zu zwingen. Das Framework kann selbst so geformt werden, dass es das anstehende Problem löst. Formulare gehören zu den Aspekten beim Erstellen einer Website, die Entwickler entweder lieben oder verabscheuen, weil die Validierung so komplex ist und sie die User auf
175
7 Benutzerauthentifizierung und Zugriffskontrolle freundliche Weise über Probleme im Formular unterrichten müssen. Im nächsten Kapitel wird es darum gehen, wie das Erstellen und Validieren von Online-Formularen durch Zend_Form vereinfacht wird.
176
8 8
Formulare
Die Themen dieses Kapitels
Einführung in Zend_Form Zend_Form Schritt für Schritt Filtern und Validieren von Formulardaten Markup und Styling von Formularen Wer schon einmal dabei war, als sich ein Krabbelkind etwas ahnungslos in den Mund gestopft hat, und darüber zu Recht erschrocken war, kann sich die Sorgen eines Entwicklers ausmalen, der Formulare erstellt. Jede Öffnung in der Applikation zieht Fragen nach Sicherheit und Stabilität mit sich. So wie ein biologischer Körper seine Schutzmaßnahmen braucht, ist es auch bei Formularen. Das ist aber nur die eine Seite: Wir müssen uns nicht nur überlegen, wie wir unsere Formulare schützen, sondern sie müssen auch gut und einfach nutzbar sein. Ohne angemessenen Testzeitraum kann man die Brauchbarkeit eines Formulars schwerlich beurteilen, was gleichzeitig eine implizite Anforderung an das Design von Formularen mit sich bringt: Sie müssen flexibel sein. Entdeckte Schwachstellen, Probleme mit der Nutzbarkeit, Aktualisierungen und Ergänzungen der zugrunde liegenden Architektur – all das kann eine Überarbeitung des Formulars erforderlich machen. Mit einem flexiblen System können solche Änderungen prompt und ohne viel Aufwand umgesetzt werden. Zend Framework verfügt über viele Optionen für das Design von Formularen: von der MVC-Architektur bis zu seinen Datenfiltern und Validatoren. In den vorigen Kapiteln wurden verschiedene Ansätze zur Erstellung von Formularen vorgestellt. In diesem Kapitel schauen wir uns nun die Komponente Zend_Form an, die genau für diese Aufgabe erstellt wurde.
177
8 Formulare
8.1
Die Arbeit mit Zend_Form Weil es Zend_Form erst seit Zend Framework 1.5 gibt, mussten alle, die sich schon länger mit diesem Framework beschäftigen, ohne Zend_Form klarkommen. Das hieß, dass ein paar von uns Zend_Form ein wenig argwöhnisch betrachteten, als es schließlich veröffentlicht wurde, und erst von seinem Wert überzeugt werden mussten. Wir sind froh, sagen zu können, dass zwar noch die eine oder andere Macke drinsteckt, aber dass wir es immer besser finden, je länger wir es kennen. In einem allgemeineren Sinn bestehen die drei Hauptvorteile der Arbeit mit Zend_Form in der schnellen Applikationsentwicklung, in der Flexibilität und der Konsistenz. Doch Sie müssen schon ein wenig mit der Komponente vertraut sein, bevor diese Vorteile offenkundig werden. Dieses Kapitel setzt sich zum Ziel, Sie auf diese Stufe zu bringen. Gegen Ende des Kapitels arbeiten wir an einem Login-Formular für die Places-Applikation, doch vorher beschäftigen wir uns noch mit einigen Features von Zend_Form.
8.1.1
Integrierte Datenfilter und Validatoren
Wenn Sie das Sicherheits-Mantra Input filtern, Output escapen kennen, dann wissen Sie sicher auch, dass alle Formulare die User-Eingaben validieren und säubern müssen sowie den Output, der in eine Datenbank eingefügt oder in einem View-Skript dargestellt werden soll, escapen müssen. und Zend_Validate sind unabhängige Komponenten, die bei Zend_Form, aber auch alleinstehend verwendet werden, wie gleich gezeigt wird. Tabelle 8.1 listet die aktuellen Filterklassen auf, die in allen Formularen verfügbar sind (einschließlich der Zend_Form-Formulare). Zend_Filter
Tabelle 8.1 Standardklassen für Zend_Filter, die mit Zend_Form verwendet werden können
178
Zend_Filter-Standardklasse
Funktion
Alnum
Entfernt alles außer alphanumerischen Zeichen
Alpha
Entfernt alles außer alphabetischen Zeichen
Digits
Entfernt alles außer Ziffern.
HtmlEntities
Konvertiert Zeichen in ihre HTML-Entity-Äquivalente, wo möglich
Int
Entspricht (int).
StringToLower
Konvertiert alphabetische Zeichen in Kleinbuchstaben.
StringToUpper
Konvertiert alphabetische Zeichen in Großbuchstaben.
8.1 Die Arbeit mit Zend_Form Zend_Filter-Standardklasse
Funktion
StringTrim
Entfernt Zeichen am Anfang und am Ende; entspricht trim().
StripTags
Schneidet alle HTML- und PHP-Tags aus.
Vielleicht fragen Sie sich, wofür einige der Filter in Tabelle 8.1 gut sein sollen, z. B. Int oder StringTrim. Was könnte einfacher sein, als einen Integer mit (int) umzuwandeln oder Zwischenraumzeichen (white space) mit trim() zu entfernen? Warum wird dazu extra eine Klasse gebraucht? Die Antwort lautet, dass so mehrere Filter verkettet und die Daten hindurchgeschickt werden können, wie es in Listing 8.1 demonstriert wird. Listing 8.1 Input-Daten über verkettete Zend_Filter-Filter verarbeiten require_once 'Zend/Filter.php'; require_once 'Zend/Filter/StringTrim.php'; require_once 'Zend/Filter/Alnum.php'; $filterChain = new Zend_Filter(); $filterChain->addFilter(new Zend_Filter_Alnum()) ->addFilter(new Zend_Filter_StringToLower()); $username = $filterChain->filter($_POST['password']);
Nehmen wir an, dass der User „MyPassword123\r“ eingetippt hat. Die Filterkette aus Listing 8.1 schneidet die nicht-alphabetischen und nicht-numerischen Zeichen aus und wandelt den Rest in Kleinbuchstaben um. Dann wird „mypassword123“ zurückgegeben. Zwar ist das sicherlich sauberer, doch für sich genommen auch ein wenig beunruhigend. Schließlich sollten wir nicht davon ausgehen, dass der User das als sein Passwort gemeint hat, auch wenn unsere Applikation es so nicht akzeptiert. Wir hätten stattdessen mit Zend_Validate überprüfen sollen, ob der Input unseren Anforderungen entspricht, und dem User eine Chance geben sollen, sein Passwort zu ändern, wenn es nicht akzeptiert wird. Tabelle 8.2 zeigt die integrierten Validierungsklassen, die wir für eine solche Prüfung hätten verwenden können. Tabelle 8.2 Standardklassen für Zend_Validate, die mit Zend_Form verwendet werden können Standard-Zend_Validate-Klasse Funktion Alnum
Daten nur mit alphanumerischen Zeichen geben true zurück.
Alpha
Daten nur mit alphabetischen Zeichen geben true zurück
Barcode
Daten, die anhand des barcode-Validierungsalgorithmus validiert werden, geben true zurück
Between
Daten zwischen den Minimal- und Maximalwerten geben true zurück.
Ccnum
Daten, die den Luhn-Algorithmus für Kreditkartennummern befolgen, geben true zurück.
179
8 Formulare Standard-Zend_Validate-Klasse Funktion Date
Daten eines spezifischen Datumsformats geben true zurück.
Digits
Daten nur mit Ziffern geben true zurück.
EmailAddress
Validiert eine E-Mail-Adresse.
Float
Daten mit einem Gleitkommawert geben true zurück.
GreaterThan
Daten größer als ein Minimum geben true zurück.
Hex
Daten nur mit hexadezimalen Zeichen geben true zurück.
Hostname
Validiert anhand von Spezifikationen einen Hostnamen.
InArray
Daten mit dem Wert „nadel“ in einem Array namens „heuhaufen“ geben true zurück.
Int
Daten, die ein valider Integer sind, geben true zurück.
Ip
Daten, die eine valider IP-Adresse sind, geben true zurück.
LessThan
Daten kleiner als ein Minimum geben true zurück.
NotEmpty
Daten, die nicht leer sind, geben true zurück.
Regex
Daten, die mit einem regulären Ausdrucksmuster übereinstimmen, geben true zurück.
StringLength
Daten mit einer Mindest-String-Länge, die ein bestimmtes Maximum nicht überschreiten, geben true zurück.
Die Tabelle 8.2 als Referenz zeigt, dass wir unser Passwort mit StringLength und Alnum validieren können. Wenn es durch die Validierungskette in Listing 8.2 geschickt wird, würde das ursprüngliche Passwort unseres Users die isValid()-Prüfung nicht bestehen, und wir könnten ihn zur Korrektur seiner Eingaben zum Formular zurückschicken. Listing 8.2 Mit Validierungsketten mehrere Eingabedatenprüfungen durchführen require_once 'Zend/Validate.php'; require_once 'Zend/Validate/StringLength.php'; require_once 'Zend/Validate/Alnum.php'; $validatorChain = new Zend_Validate(); $validatorChain->addValidator(new Zend_Validate_StringLength(6, 15)) ->addValidator(new Zend_Validate_Alnum());
if ($validatorChain->isValid($_POST['password'])) { // Passwort entspricht unseren Anforderungen }
180
8.1 Die Arbeit mit Zend_Form Die Beispiele in den Listings 8.1 und 8.2 demonstrierten, wie man Daten unabhängig validieren und filtern kann. Später in diesem Kapitel werden wir uns anschauen, wie der Prozess deutlich einfacher verwaltbar ist, indem diese Komponenten in Zend_Form integriert werden.
8.1.2
Integrierte Fehlerbehandlung
Bei der Diskussion von Listing 8.2 erwähnten wir, dass der User zu seinem Formular zurückkehren soll, um die Eingaben zu verändern. Das kann man etwa mit folgendem Code erledigen: $this->view->formMessages = $validatorChain->getMessages();
Diese Nachrichten könnte man dem User dann im View-Skript zeigen: formMessages): ?>
formMessages as $message): ?>
Wenn man das für jedes Element machen muss, artet es wirklich in Arbeit aus. Doch zum Glück ist die Fehlerbehandlung in Zend_Form integriert, und zwar in Form von Dekoratoren, die die Menge der erforderlichen Programmierarbeit reduzieren, um den gleichen Effekt zu erzielen.
8.1.3
Dekoratoren zur Vereinfachung des Markups
Das vorige Beispiel einer Fehlerbehandlung veranschaulicht einen Teil der beim FormularMarkup erforderlichen Arbeit über das Formular-HTML selbst bis zu den Hinweisen zu den Fehlern, die ein User möglicherweise gemacht hat. Stellen Sie sich vor, wie viel Markup für ein langes Formular nötig ist, und Sie können sich ausmalen, was das für eine Schreib- und Organisationsarbeit für diesen Code ist. Zend_Form-Dekoratoren
basieren auf dem gleichnamigen Designpattern Dekorator, und damit ihnen kann man im Wesentlichen Formularelemente in Schichten von Markup einfassen. Tabelle 8.3 listet die in verfügbaren Zend_Form-Standarddekoratoren auf.
Tabelle 8.3 Zend_Form-Standarddekoratoren zur Auszeichnung von Formularelementen
Zend_Form-Dekoratorklasse
Funktion
Callback
Führt einen Callback zum Rendern von Inhalten aus.
Captcha
Wird gemeinsam mit dem Captcha-Formularelement verwendet, um unerwünschte automatische Formularübermittlungen zu verhindern.
181
8 Formulare Zend_Form-Dekoratorklasse
Funktion
Description
Stellt eine Beschreibung von Elementen dar, z. B. für Formularhinweise.
DtDdWrapper
Umgibt Inhalte mit einer HTML-Definitionsliste.
Errors
Stellt Fehler in einer unsortierten HTML-Liste dar.
Fieldset
Umgibt Inhalte mit einem HTML-Fieldset-Element.
Form
Umgibt Inhalte mit einem HTML-Formularelement.
FormElements
Iteriert durch alle Elemente, rendert sie und führt sie zusammen.
HtmlTag
Umgibt Inhalte mit einem von Ihnen definierten HTMLElement.
Image
Fügt ein HTML-img-Element ein.
Label
Rendert Bezeichnungen für Formularelemente.
ViewHelper
Damit kann eine View-Hilfsklasse angegeben werden, die ein Element erstellt.
ViewScript
Damit kann ein View-Skript angegeben werden, das ein Element erstellt.
So wie bei Filtern und Validatoren bilden diese Dekoratorklassen nur den im Zend Framework verfügbaren Standard, und für die meisten Fälle reichen sie auch aus. Falls nicht, können sie alle angepasst werden, und in dieser Hinsicht ist das nächste Feature besonders nützlich.
8.1.4
Plug-in-Loader zur eigenen Anpassung
Wenn Ihre speziellen Bedürfnisse durch die standardmäßig verfügbaren Validatoren, Filter und Dekoratoren nicht erfüllt werden, freuen Sie sich sicher zu hören, dass man bei Zend_Form über Zend_Loader_PluginLoader eigene Alternativen einfügen kann. Wir könnten beispielsweise wie folgt einen Standort für eigene Filter angeben: $passwordElement->addPrefixPath( 'Places_Filter', 'Places/Filter/', 'filter' );
Oder für Validatoren: $passwordElement->addPrefixPath( 'Places_Validator', 'Places/Validator/', 'validate' );
182
8.1 Die Arbeit mit Zend_Form Oder für Dekoratoren: $passwordElement->addPrefixPath( 'Places_Decorator', 'Places/Decorator/', 'decorator' );
Das Gute beim PluginLoader ist, dass er Ihre alternativen Standorte vor den Standardstandorten priorisiert. Wenn wir beispielsweise eine Dekoratorklasse namens Places_Form_Decorator_Errors in library/Places/Validator/Errors.php haben, würde diese statt der Standardklasse Zend_Form_Decorator_Errors des Zend Frameworks verwendet werden, einfach weil die gleiche Namenskonvention genutzt wird.
8.1.5
Internationalisierung
In gewisser Weise erstaunlich ist, dass Zend_Form die erste Komponente war, die vollständig in die anderen Internationalisierungskomponenten von Zend Framework integriert worden ist. Wir geben vorab ein kurzes Beispiel in den folgenden Code-Beispielen, doch das Thema Internationalisierung und Lokalisierung wird noch eingehender im Kapitel 15 besprochen.
8.1.6
Unterformulare und Displaygroups
Mit Unterformularen und Displaygroups hat man zwei Möglichkeiten, um Formularelemente zu gruppieren. Displaygroups werden im Prinzip genauso eingesetzt wie das HTML-Fieldset und dann auch tatsächlich standardmäßig als Fieldset gerendert. Unterformulare sind, wie Sie sich denken können, Zend_Form- oder Zend_Form_SubFormObjekte, die innerhalb von anderen Formularen genutzt werden können. Um die Unterscheidung zu veranschaulichen, betrachten wir ein Widget-Unterformular zur Datumswahl, das wie folgt für das Geburtsdatum in einem Benutzerregistrierungsformular verwendet wird: $form->addSubForm($dateSelectorForm, 'birthdate');
Die Adressdetails des Users könnten wie folgt in einem Adress-Fieldset mit einer Displaygroup gruppiert werden: $form->addDisplayGroup( array('street', 'town', 'state','postcode'), 'address');
Das Formular, das wir erstellen werden, braucht keine Displaygroups oder Unterformulare, doch auch diese kurzen Beispiele demonstrieren ihren Wert in größeren Formularen. Unterformulare und Displaygroups sind unseres Erachtens die zentralen Features, die Zend_Form zu einer attraktiven Option machen, wenn es ums Erstellen von Formularen geht. Doch zum schlagenden Beweis wird das erst im Einsatz, aber bisher haben wir Zend_Form noch nicht sonderlich in Anspruch genommen. Im nächsten Abschnitt wählen wir einen schrittweisen Ansatz und zeigen, wie diese Features bei der Erstellung des Log-
183
8 Formulare in-Formulars zusammenarbeiten, das in Kapitel 7 zur User-Authentifizierung und Autorisierung erstellt wurde.
8.2
Ein Login-Formular erstellen Wir haben einige der verfügbaren Optionen bei der Nutzung von Zend_Form präsentiert, und natürlich gibt es davon noch eine Menge mehr. Bei Zend_Form geht es vor allem darum, in Formularelemente weitere Funktionalitätsschichten einzubauen, und das erklärt man am besten schrittweise. Aus diesem Grund halten wir uns an das einfache LoginFormular mit den beiden Feldern, das im vorigen Kapitel angeführt wurde. Um einen guten Eindruck von den durch Zend_Form möglichen Verbesserungen zu erhalten, schauen Sie sich bitte noch einmal die Beispiele aus Kapitel 7 an, wenn Sie dieses Kapitel durcharbeiten. Wir beginnen mit der Einrichtung der erforderlichen Dateien und Pfade zu ihren Standorten.
8.2.1
Pfade einrichten
Als Erstes richten wir das Verzeichnis application/forms ein, in dem wir die Formularklassen speichern, und ergänzen diesen Standort im include-Pfad der Bootstrap-Datei: set_include_path(get_include_path() . PATH_SEPARATOR . ROOT_DIR . '/library/' . PATH_SEPARATOR . ROOT_DIR . '/application/models/' . PATH_SEPARATOR . ROOT_DIR . '/application/forms/' );
Dann können wir die Hauptdateien einfügen und mit dem Programmieren anfangen.
8.2.2
Das Formular-View-Skript
Nach dem Rendern muss das Formular ja irgendwo dargestellt werden. Wir haben bereits ein auth/login.phtml-View-Skript aus dem vorigen Kapitel, das wir zum Vergleich heranziehen. In Listing 8.3 haben wir die Datei form.phtml erstellt und den Code eingefügt, um die Zend_Form-Version des gleichen Autorisierungsformulars aus dem vorigen Kapitel zu zeigen. Listing 8.3 Das Login-View-Skript auth/form.phtml nutzt Zend_Form.
Log in
Please log in here
formResponse; ?> form; ?>
184
Stellt alle Feedback-NachStellt das gerenderte richten dar Formular dar
8.2 Ein Login-Formular erstellen Wenn Sie wie wir auch schon eine Menge technischer Artikel gelesen haben, möge Ihnen verziehen werden, falls Sie bei dem Spruch „All das in nur einer Zeile Code“, der unsere Diskussion einleitet, skeptisch die Augenbrauen hochziehen. Seien Sie nachsichtig mit uns, denn die Aktualisierungen der Controller-Action-Datei sind fast ebenso beeindruckend.
8.2.3
Aktualisierung der Controller-Action AuthController
Bei Ruby on Rails kennt man das Paradigma „Skinny controller, fat model“ (etwa: dünner Controller, dickes Model), die Basisprämisse, nach der man die Controller-Dateien schlank und einfach verständlich hält, indem soviel Logik wie möglich in die Model-Dateien verschoben wird. In Kapitel 7 erstellten wir einige recht komplexe Controller-Dateien, die nun anhand von Zend_Form refakturiert werden. Wir beginnen mit einer kleineren Modifikation der indexAction()-Methode (siehe Listing 8.4). Diese prüft, ob der User identifiziert werden konnte, und falls nicht, wird er an die formAction()-Methode in Listing 8.5 weitergeleitet, wo er das Login-Formular zu sehen bekommt. Bei der späteren Entwicklung können wir eine solche else-Option einfügen, durch die der User auf seine Account-Seite weitergeleitet wird, falls er bereits eingeloggt ist. Listing 8.4 Die refakturierte Controller-Action indexAction() public function indexAction() { if (null === Zend_Auth::getInstance()->getIdentity()) { $this->_forward('form'); Leitet formAction() } weiter }
Prüft UserAuthentifizierung
Als Nächstes müssen wir eine neue Action-Methode (siehe Listing 8.5) einfügen, die sich um alle Aspekte unseres Login-Formulars kümmert: von der Verarbeitung der POSTAnfrage über das Einrichten des Formulars bis zu seiner Darstellung. Manche Teile wirken zu Anfang vielleicht unklar, doch im weiteren Verlauf wird alles erklärt. Wir zeigen es hier, weil es später nicht mehr verändert wird.
185
8 Formulare Listing 8.5 Die formAction() in der Controller-Action-Klasse AuthController public function formAction() { $form = new LoginForm('/auth/form/'); $this->view->formResponse = ''; if ($this->_request->isPost()) { if ($form->isValid( $this->_request->isPost()) ) {
$data = $authAdapter->getResultRowObject( aus authAdapter null, 'password'); $auth = Zend_Auth::getInstance(); Schreibt Autorisier$auth->getStorage()->write($data); ungsinfo in Storage $this->_redirect($this->_redirectUrl); } else { Setzt Nachricht für Leitet $this->view->formResponse = ' Formularantwort User um Sorry, there was a problem with your submission. Please check the following:'; }
} $this->view->form = $form;
Setzt View-Variable für Formular
}
Die formAction()-Methode instanziiert als Erstes das Login-Zend_Form-Objekt und übergibt ihm die URL, die im Action-Attribut des HTML-Formularelements verwendet werden soll n. Nach Prüfung, ob es sich bei der Anfrage um einen HTTP POST handelt und somit um eine Formularübermittlung o, werden die übermittelten Daten als Parameter an die Methode isValid() übergeben, die einen Booleschen Wert zurückgibt, durch den wir die nächste Action bestimmen können p. Wenn die Formulareingaben valide sind, lesen wir anhand des Username-Formularelements das authAdapter-Objekt aus dem selbst erstellten Validator Places_Validate_ Authorise aus q, und von dort bekommen wir die Datenbankzeile, in der die Autorisierungsinformationen des Users stehen (aus Sicherheitsgründen fehlt hier aber das Passwort) r. Dann können wir über Zend_Auth die Datenbankzeile anhand der jeweils angegebenen Speichermethode speichern (standardmäßig ist das sessions) s. Schließlich leiten wir den User auf eine beliebige Seite um t. Wenn die übermittelten Daten nicht valide sind, informieren wir den User darüber, dass es Probleme gibt u, die er dann im an die View gesandten und gerenderten Formular sehen kann v. An diesem Punkt ist es erwähnenswert, dass man bei Bedarf auch über den folgenden Code die Werte im Formular hätte setzen können: $form->populate($this->_request->isPost());
186
8.2 Ein Login-Formular erstellen Das wäre dann angemessen gewesen, wenn wir das Formular bereits mit Standardwerten hätten befüllen wollen. Weil dies ein Login-Formular ist, brauchen wir das nicht zu machen, und nebenbei bemerkt befüllt die isValid()-Methode das Formular automatisch mit den POST-Werten. Nachdem die Controller-Action nun fertig ist, können wir mit der Erstellung der Klasse LoginForm weitermachen. Das geschieht in verschiedenen aufeinander folgenden Schritten, beginnend mit der einfachsten Version.
8.2.4
Die Basisklasse für das Login-Formular
Wir beginnen mit dem Erstellen mit den wenigsten Elementen, die zum Rendern eines Login-Formulars erforderlich sind, das aus den Feldern für Username und Passwort sowie dem Submit-Button besteht. Im ersten Schritt (siehe Listing 8.6) erstellen wir die Datei LoginForm.php in /application/forms/ und fügen eine loginForm-Klasse ein, die Zend_Form erweitert. Listing 8.6 Das Login-Basisformular mit Zend_Form include_once 'Zend/Form.php'; class LoginForm extends Zend_Form { public function init($action) Erstellt { HTML-Formularelement $this->setAction($action) ->setMethod('post') ->setAttrib('id', 'loginForm'); $username = $this->addElement('text', 'username', array('label' => 'Username')); $password = $this->addElement('password', 'password', array('label' => 'Password')); $submit = $this->addElement('submit', 'Login'); } }
Erstellt Textelement username
Erstellt passwordElement
Erstellt submit-Button
Bei diesem Code wäre zuerst einmal anzumerken, dass das Formular anhand der init()Methode eingerichtet wird: Das ist eine Convenience-Methode, die aus dem Konstruktor von Zend_Form heraus aufgerufen wird. Darüber werden die Komponenten des Formulars eingefügt, und zwar beginnend mit dem Formularelement und dessen verschiedenen Attributen, gefolgt von den anderen Elementen des Formulars. Weiter erwähnenswert ist, dass unsere Elemente über das Designpattern Factory-Method zum Einfügen von Elementen erstellt werden. Die Alternative ist, die Elemente selbst zu instanziieren und sie wie folgt dem Formular zu übergeben: $username = new Zend_Form_Element_Text('username'); $this->addElement($username);
187
8 Formulare Wir haben uns für den Factory-Ansatz entschieden, weil er den Vorteil hat, dass alle von uns im Formularobjekt vorgenommenen Einstellungen vererbt werden. Zusammen mit der bereits erstellten Controller-Action wird dieses Formular seinen Zweck erfüllen, solange die übermittelten Daten immer den Anforderungen der Applikation entsprechen. Wir alle wissen, dass das nicht immer der Fall ist, weil die User oft unkorrekte oder leere Felder abschicken, um die wir uns durch Validierung und Filtern der Daten kümmern müssen.
8.3
Filtern und Validieren Zu Anfang dieses Kapitels erwähnten wir, dass Zend_Filter und Zend_Validate in Zend_Form integriert sind, und hier bekommen wir die Chance zu zeigen, wie dadurch ihre Nutzung vereinfacht wird.
8.3.1
Einfaches Filtern und Validieren
In Listing 8.7 wurden die Elemente Username und Passwort anhand des Fluent-Interface der Methoden addValidator() und addFilter() um das Filtern und Validieren erweitert. Listing 8.7 Die Login-Formularelemente mit Filterung und Validierung ergänzen $this->addElement('text', 'username', array('label' => 'Username')); $username = $this->getElement('username') ->addValidator('alnum') ->setRequired(true) ->addFilter('StringTrim'); $this->addElement('password', 'password', array('label' => 'Password')); $password = $this->getElement('password') ->addValidator('stringLength', true, array(6)) ->setRequired(true) ->addFilter('StringTrim');
All diese Ergänzungen beruhen auf den namensverwandten Komponenten Zend_Filter oder Zend_Validate; sogar setRequired() verwendet den Validator NotEmpty. Die Rolle des Validators ist, alle übermittelten Daten hervorzuheben, die die Validierung nicht bestehen. Das kann dann vom Fehlerdekorator verwendet werden, um auf Felder hinzuweisen, die zum Rendern des Formulars einer Korrektur bedürfen. Abbildung 8.1 zeigt die Fehlermeldungen im gerenderten Formular, nachdem ein leeres Formular übermittelt wurde.
188
8.3 Filtern und Validieren
Abbildung 8.1 Das Resultat der Übermittlung eines leeren Formulars
Neben der erforderlichen Einstellung, die dafür sorgt, dass das Element vor Übermittlung einen Wert bekommt, bestanden wir darauf, dass Usernamen alphanumerisch sind und keine Leerzeichen enthalten, und dass Passwörter mindestens sechs Zeichen aufweisen, aber auch keine Leerzeichen. Wenn das Formular dann mit Informationen übermittelt wird, die diesen Anforderungen nicht genügen, wird man auf das Formular zurückgeleitet und bekommt die Infos aus Abbildung 8.2 zu sehen.
Abbildung 8.2 Das Resultat der Übermittlung nicht-valider Daten
Fehlermeldungen wie „'@@@@@@' has not only alphabetic and digit characters“ und „'***' is less than 6 characters long“ passen wahrscheinlich nicht zu der Art von Applikation, die Sie erstellen wollen. Zum Glück ist es sehr einfach, eigene Nachrichten zu integrieren.
8.3.2
Eigene Fehlermeldungen
Für das Einfügen eigener Meldungen in Validatoren braucht nur der Validator über seinen Namen angesprochen zu werden, und dann wird die Nachricht über dessen Methode setMessage()entsprechend geändert. Diese weiteren Ergänzungen für unsere Formularelemente stehen in Listing 8.8. Listing 8.8 Eigene Meldungen in die Login-Formularelemente einfügen $this->addElement('text', 'username', array('label' => 'Username')); $username = $this->getElement('username') ->addValidator('alnum') ->setRequired(true) ->addFilter('StringTrim'); $username->getValidator('alnum')->setMessage( 'Your username should include letters and numbers only'); $this->addElement('password', 'password',
189
8 Formulare array('label' => 'Password')); $password = $this->getElement('password') ->addValidator('stringLength', true, array(6)) ->setRequired(true) ->addFilter('StringTrim'); $password->getValidator('stringLength')->setMessage( 'Your password is too short');
Wenn wir nun die gleichen Informationen wie vor Veränderung der Nachrichten übermitteln, bekommen wir ein für die Places-Site viel passenderes Feedback (siehe Abbildung 8.3). Allerdings sollte angemerkt werden, dass diese Nachrichten zwar für unsere User sehr hilfreich sind, aber dass das Formular nun ein wenig zuviel Feedback-Informationen enthält, was jenen in die Hände spielt, die in die Site einbrechen wollen. Das sollten Sie beim Erstellen eigener Formulare natürlich berücksichtigen.
Abbildung 8.3 Das Login-Formular mit selbst erstellten Fehlermeldungen
Places konzentriert sich aktuell darauf, erst einmal in Großbritannien lebende Familien zu versorgen. Also brauchten wir hier nur die englischen Nachrichten freundlicher zu formulieren. Wenn die Site so bekannt wird, dass auch andere europäische Länder mit einbezogen werden, müssen wir uns darum kümmern, dass die Formulare und andere Inhalte ebenfalls internationalisiert werden.
8.3.3
Die Internationalisierung des Formulars
Es ist interessant, dass Zend_Form die erste Komponente des Zend Frameworks ist, die in die Komponenten für die Internationalisierung aufgenommen werden. Der Vorteil für unsere Zwecke ist die Einfachheit, mit der wir das Formular so ändern können, dass es Fehlermeldungen in einer anderen Sprache anzeigt, z. B. die deutsche Version in Listing 8.9.
190
8.3 Filtern und Validieren Listing 8.9 Das Login-Formular mit übersetzten Fehlermeldungen public function init() Bindet Übersetzungs{ include ROOT_DIR . datei ein '/application/configuration/translations/de_DE.php'; $translate = new Zend_Translate( 'array', $translationStrings, 'de'); Instanziiert $this->setTranslator($translate); $this->setAction('/auth/index/') ->setMethod('post') ->setAttrib('id', 'loginForm');
Die Übersetzungen aller Elemente basieren auf dem Wert für die in jeder Validator-Klasse enthaltenen Konstanten. Für Zend_Validate_StringLength ist es also die Konstante const TOO_SHORT = 'stringLengthTooShort';
Der Wert stringLengthTooShort ist derjenige, den wir als Schlüssel in unserem Übersetzungs-Array verwenden: 'stringLengthTooShort' => 'Ihr Kennwort ist zu kurz'
Nachdem wir alle Übersetzungsdaten eingefügt haben, werden die Fehlermeldungen nun automatisch in die angegebene Sprache übersetzt (siehe Abbildung 8.4).
Abbildung 8.4 Das ins Deutsche übersetzte Login-Formular
Ihnen fällt wahrscheinlich auf, dass die Nachrichten zwar auf Deutsch sind, aber der Submit-Button immer noch auf Englisch ist. Das bedeutet, dass unsere Übersetzung unvoll-
191
8 Formulare ständig ist. In Kapitel 15 werden wir uns eingehender mit der Internationalisierung beschäftigen. Jetzt machen wir aber mit der Erstellung eines eigenen Validators weiter.
8.3.4
Selbst erstellte Validatoren
Für das Login-Formular müssen wir noch prüfen können, ob der User registriert ist oder nicht. Das könnten wir auch als weiteren Schritt in der Controller-Action-Methode formAction() einfügen, doch in Abschnitt 8.1.4 haben wir beschrieben, wie man anhand von Plug-in-Loadern eigene Filter, Validatoren und Dekoratoren ins Formular einbauen kann. In Listing 8.10 passiert genau das mit einem eigenen Authorise-Validator. Listing 8.10 Das username-Element mit einem eigenen Authorise-Validator $username = $this->addElement('text', 'username', array('label' => 'Username')); $username = $this->getElement('username') ->addValidator('alnum') ->setRequired(true) Gibt eigenen ->addFilter('StringTrim') Validator-Standort an ->addPrefixPath('Places_Validate', 'Places/Validate/', 'validate') ->addValidator('Authorise'); Fügt eigenen $username->getValidator('alnum') Validator ein ->setMessage( 'Your username should include letters and numbers only');
Der in Listing 8.11 gezeigte Authorise-Validator prüft die Inhalte der Felder Username und Passwort und validiert das Formular nur, wenn beide die Prüfung bestehen. Wir hängen ihn an das username-Feld an, was bedeutet, dass dessen Fehlermeldungen unter dem Username-Feld dargestellt werden. Dafür haben wir die Nachricht entsprechend vage gehalten, also sollte das kein echtes Problem darstellen. Listing 8.11 Ein eigener Authorise-Validator
Definiert Klassenname class Places_Validate_Authorise anhand korrekter Namenskonvention extends Zend_Validate_Abstract { const NOT_AUTHORISED = 'notAuthorised'; protected $_authAdapter; protected $_messageTemplates = array( self::NOT_AUTHORISED => 'No users with those details exist' ); public function getAuthAdapter() { return $this->_authAdapter; } public function isValid($value, $context = null) {
192
Fügt erforderliche
isValid()-Methode ein
8.3 Filtern und Validieren $value = (string) $value; $this->_setValue($value); if (is_array($context)) { if (!isset($context['password'])) { return false; } }
Wer das vorige Kapitel gelesen hat, dem sollte die selbst erstellte Validierungsklasse Authorise bekannt vorkommen. Also halten wir uns an die Ergänzungen, die wir vornehmen müssen, um sie zu einem Validatoren zu machen. Zuerst muss Zend_Validate_Abstract erweitert werden, dann wird das Namenspräfix benutzt, das wir dem Plug-in-Loader in der loginForm-Klasse gegeben haben n, und in Places/Validate/Authorise.php abgelegt. Es muss auch eine isValid()Methode enthalten o, durch die der Wert des anhängten Elements (in diesem Fall username) automatisch gelotst wird, und auch das $context-Array aller anderen Formularwerte. Places_Validate
Weil wir sowohl den Usernamen als auch das Passwort brauchen, um zu bestimmen, ob ein User registriert ist, prüfen wir zuerst, ob das $context-Array verfügbar ist und ob es ein Passwortelement enthält p. Wenn alle Informationen vorhanden sind und sich der User als valide herausgestellt hat, wird der Validator als Antwort true zurückgeben; anderenfalls gibt er false zusammen mit anderen möglichen Fehlermeldungen zurück, die man dann dem User zeigen kann. Nachdem wir eigene Validatoren eingefügt haben, ist die Backend-Funktionalität des Formulars vollendet, und wir können uns dem Frontend und den Dekoratoren zuwenden.
193
8 Formulare
8.4
Die Dekoration des Login-Formulars Wie bereits in diesem Kapitel schon erwähnt, dienen Dekoratoren dem Einfügen von Markup-Schichten in Formulare und Formularelemente. In diesem Abschnitt nutzen wir sie, um das HTML-Markup unseres Formulars zu ändern.
8.4.1
Standarddekoratoren von Zend_Form
Bisher haben wir die Standarddekoratoren verwendet, die wie folgt in Zend_Form_Element geladen werden: $this->addDecorator('ViewHelper') ->addDecorator('Errors') ->addDecorator('HtmlTag', array('tag' => 'dd')) ->addDecorator('Label', array('tag' => 'dt'));
Wir haben auch diejenigen aus Zend_Form verwendet, und zwar so: $this->addDecorator('FormElements') ->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')) ->addDecorator('Form');
Für das Login-Formular produzieren diese Standardeinstellung das in Listing 8.12 gezeigte Markup, was im Wesentlichen die Formularelemente darstellt, die von einer HTMLDefinitionsliste umschlossen sind. Listing 8.12 Das von den Standarddekoratoren produzierte HTML für das Login-Formular
Gibt man da noch ein wenig CSS hinzu, kann eine Definitionsliste sehr effektiv gestylt werden, doch für unsere Zwecke wollen wir das Formular lieber in eine etwas einfachere unsortierte HTML-Liste einfassen. Das ist nun an der Reihe.
8.4.2
Eigene Dekoratoren setzen
Bei der Arbeit mit dem Zend Framework werden Sie merken, dass es oft mehrere Wege gibt, den Code zu schreiben. Das gilt auch für Dekoratoren. Behalten Sie also im Hinterkopf, dass es nicht der einzige gangbare Weg ist, wie wir in Listing 8.13 die Einstellungen für die Dekoratoren vornehmen. Wir werden das im weiteren Verlauf erläutern.
194
8.4 Die Dekoration des Login-Formulars Listing 8.13 Das Login-Formular mit den Einstellungen für die Dekoratoren public function init() { $this->setAction('/auth/index/') ->setMethod('post') ->setAttrib('id', 'loginForm'); $this->addElementPrefixPath('Places_Validate', 'Places/Validate/', 'validate');
Bevor wir eigene Dekoratoreneinstellungen einfügen, müssen wir zuerst die Standarddekoratoren über die clearDecorators()()-Methode deaktivieren n. Anschließend richten wir ein Standard-Array von Elementdekoratoren ein o. Unsere Einstellungen sind denen der Standarddekoratoren nicht ganz unähnlich, abgesehen von dem eingefügten Klassennamen leftalign fürs HTML-Label sowie einem Sternchen-Suffix für alle erforderlichen Elemente (nicht unbedingt nötig in diesem Fall, doch zur Veranschaulichung hier aufgeführt) und fassen das alles in ein li-HTML-Tag ein. Das resultierende HTML sieht wie folgt aus:
Ein Vorteil der Arbeit mit der Factory-Methode (um Formularelemente einzufügen) ist, dass wir alle Elementdekoratoren auf einmal setzen können. Sie erben unsere Dekoratoreneinstellung durch diese Code-Zeile: $this->setElementDecorators($decorators);
Bei nur zwei Formularfeldern wirkt sich dieser Komfort kaum aus, also setzen wir unsere Dekoratoren jeweils pro Element p, außer beim Submit-Button, der individuell gesetzt wird, weil bei ihm keine Label- oder Fehlermeldungen erforderlich sind q. Zum Abschluss setzen wir den Formulardekorator insgesamt r. Die Reihenfolge ist wichtig, in der die Dekoratoren eingefügt werden, und in diesem Fall ergänzen wir die Formularelemente zuerst, gefolgt von einem ul-HTML-Tag und fassen alles in ein HTMLdiv-Tag ein (klingt ein bisschen nach Kochrezept, oder?). Folgendes gilt es noch zu beachten: Weil wir das Einfassen erst mit einem HtmlTag vornehmen, dem ein weiteres folgt, müssen wir dem zweiten einen „Alias“ geben. Anderenfalls werden mehrere Dekoratoren des gleichen Typs einander überschreiben. In diesem Fall bekommt der zweite HtmlTag-Dekorator das Alias DivTag, über den es eindeutig identifiziert wird s. Zugegeben: Für unser Formular ist dieses letzte div-Tag nicht erforderlich, doch aus Gründen der Demonstration wollen wir annehmen, dass es eine Anforderung der Designer ist. In Listing 8.14 sehen wir, dass sich das komplette HTML, das nach den Veränderungen generiert wurde, deutlich vom originalen Standard-HTML aus Listing 8.12 unterscheidet.
196
8.4 Die Dekoration des Login-Formulars Listing 8.14 Das von den selbst vorgenommenen Dekoratoreinstellungen produzierte HTML für das Login-Formular
Obwohl wir das Markup bereits beträchtlich anpassen konnten, erlaubt Zend_Form sogar eine noch komplexere Anpassung. Ob Sie das durch Erstellen eigener Dekoratoren machen, die Zend_Decorator_Abstract erweitern, oder mit eigenen View-Hilfsklassen oder gar einfach mit eigenen View-Skripten, hängt ganz von Ihren jeweiligen Anforderungen ab. Doch wir möchten Sie dran erinnern, dass das HTML so einfach wie möglich gehalten und das Styling so weit wie möglich im CSS vorgenommen werden sollte. 8.4.2.1
Styling mit CSS
Aus Gründen der Vollständigkeit schließen wir nun die Erstellung des Formulars ab, indem wir die Veränderungen demonstrieren, die auch schon das kleinste CSS (siehe Listing 8.15) bei dem einfachen HTML bringen, das unsere neuen Dekoratoreneinstellungen produzieren. Listing 8.15 Die CSS-Regeln in form.css zur Gestaltung des Login-Formulars form div#loginDiv {width: 400px; } form ul { list-style: none; margin: 0; padding: 0; } form ul li { margin: 0; clear: both; border-top: 1px dotted #666; padding: 0.5em 0; } label.leftalign { display: block; float: left; font-weight: bold; padding-right: 10px;
In Abbildung 8.5 sehen Sie, wie diese CSS-Regeln das Login-Formular verbessern, indem die Label linksbündig angeordnet und die Formularfelder durch Rahmen besser unterscheidbar gemacht werden. Das ist noch um einiges ausbaufähig, wäre das Styling von Formularen mit CSS der Schwerpunkt dieses Buches.
Abbildung 8.5 Das neu dekorierte und mit CSS gestylte Login
Dieses einfache Login-Formular ist nun übersetzbar und filtert, validiert und produziert nicht nur eigene Fehlermeldungen, sondern kann überdies auch so gestaltet werden, dass es den Anforderungen unserer Applikation entspricht.
8.5
Zusammenfassung Wir hoffen, dass Ihnen die Arbeit mit Zend_Form nun vertraut ist und Sie die damit mögliche Flexibilität besser erkennen. Die Vorteile der Arbeit mit Zend_Form werden deutlicher, wenn Sie eine Library mit Komponenten erstellen, die bei verschiedensten Applikationen eingesetzt werden können. Wenn man so wie wir die Formulare in Unterklassen von Zend_Form separiert, verbessert das die Struktur der Applikation und erleichtert das Testen. Leider gibt es eine Menge, das wir hier nicht ansprechen konnten, beispielsweise den Einsatz von Unterformularen und die weitere Untersuchung der Anpassungsoptionen. Zend_Form verfügt über soviel Funktionalitäten, dass man mehrere Kapitel damit füllen könnte, doch wir müssen nun mit anderen Themen weitermachen. Mehr Informationen über Zend_Form erhalten Sie im Blog unseres Fachkorrektors Matthew Weier O’Phinney unter http://weierophinney.net/matthew/.
198
9 9
Suchfunktionen
Die Themen dieses Kapitels
Die Komponente Zend_Search_Lucene Integration einer Suchseite in eine Applikation Das Observer-Pattern und wie es die Code-Separation unterstützt Eine ausgezeichnete Website unterscheidet sich von einer mittelmäßigen unter anderem durch das Feature einer Suchfunktion. Egal, wie gut Ihr Navigationssystem ist – Ihre User sind an Google gewöhnt und erwarten, auch Ihre Website durchsuchen zu können. Wenn sie dort nicht selbst suchen können oder die Suchfunktion keine nützlichen Resultate erbringt, wandern sie ab zur nächsten Website – und probieren so lange alle durch, bis sie fündig werden. Ein gutes Suchsystem ist schwer zu schreiben, doch das Zend Framework enthält zur Vereinfachung die Komponente Zend_Search_Lucene. In diesem Kapitel schauen wir uns an, wie die Suchfunktion für den User arbeiten soll und wie die Komponente Zend_Search_Lucene funktioniert. Anschließend integrieren wir die Suche auf der Places-Website und schauen uns an, wie man ein Model indexiert und die Resultate dem User präsentiert.
9.1
Die Vorteile einer Suchfunktion Wir haben am meisten von einer Website, wenn wir schnell und sicher die Informationen finden, nach denen wir suchen. Bei einer E-Commerce-Site bedeutet das in der Regel, dass man die gewünschte Ware findet und kauft. Bei anderen Sites suchen wir nach relevanten Artikeln über für uns interessante Themen. Die Suchfunktion ist eine Möglichkeit, wie ein User schnell an seine gewünschten Resultate kommt. Ein gutes Suchsystem stellt relevante Ergebnisse ganz weit oben dar.
199
9 Suchfunktionen
9.1.1
Zentrale Usability-Probleme der User
User wollen von einer Suchfunktion nur eins: das Richtige finden, und das mit minimalem Aufwand. Natürlich ist das nicht einfach, und das ist einer der Hauptgründe, warum Google unter den Suchmaschinen die Nummer Eins ist. Generell reicht dafür ein einfaches Textfeld. Der User gibt seinen Suchbegriff ein, löst mit Klick auf die Suchen-Schaltfläche den Vorgang aus und bekommt eine Liste mit relevanten Antworten.
9.1.2
Die Rangliste der Resultate ist wichtig
Damit eine Suchfunktion überhaupt zu etwas nutze ist, ist es wichtig, dass die dargestellten Resultate relevant sind. Das erfordert die Einordnung der Resultate in eine Rangliste, und zwar in der Reihenfolge der Relevanz für die Suchanfrage. Dafür wird eine Volltextsuchmaschine eingesetzt. Solche Suchmaschinen arbeiten so, dass sie eine Liste mit Schlüsselwörtern für jede Seite der Site erstellen: den sogenannten Index. Neben den Schlüsselwörtern werden auch andere relevante Daten wie Titel, Erstellungsdatum des Dokuments, Autor sowie Informationen, wie man die Seite auslesen kann (z. B. den URL) gespeichert. Startet der User dann eine Suchanfrage, wird jedes Dokument im Index danach beurteilt, wie viele der angefragten Schlüsselwörter darin vorkommen. Das Resultat wird dann so dargestellt, dass die relevanten Seiten ganz oben kommen. Mit PHP ist es sehr einfach, ein simples Suchsystem anhand einer einfachen Datenbankabfrage zu erstellen. Die Schwierigkeit besteht darin, für die Resultate ein Ranking vorzunehmen, bei dem die relevantesten Dokumente am Anfang stehen. Zur Lösung dieses Problems dient Zend_Search_Lucene.
9.2
Die Komponente Zend_Search_Lucene Die Suchfunktionskomponente Zend_Search_Lucene des Zend Frameworks ist ein sehr leistungsfähiges Tool. Dabei handelt es sich um eine auf dem bekannten Projekt Apache Lucene (eine Suchmaschine für Java) basierende Volltextsuchmaschine. Die von Zend_ Search_Lucene erstellten Indexdateien sind mit Apache Lucene kompatibel. Also funktionieren alle dafür geschriebenen Utilities zur Indexverwaltung auch mit Zend_Search_ Lucene. erstellt einen aus Dokumenten bestehenden Index. Die Dokumente sind alle Instanzen von Zend_Search_Lucene_Document, und alle enthalten Zend_Search_ Lucene_Field-Objekte, in denen die eigentlichen Daten stehen. Das veranschaulicht Abbildung 9.1. Zend_Search_Lucene
200
9.2 Die Komponente Zend_Search_Lucene Index für Zend_Search_Lucene
ku m en
t
Zend_Search_Lucene_Document
Do
Zend_Search_Lucene_Document
er
in
ei
ne
m
Zend_Search_Lucene_Document
e el Vi
Zend_Search_Lucene_Field::Unstored, z. B. Inhalte Zend_Search_Lucene_Field::Unindexed, z. B. Erstellungsdatum
Vi
el
e
Do
ku m en
te
in
ei
ne
m
In d
ex
Fe
ld
Zend_Search_Lucene_Field::Text, z. B. Dokumentitel
Zend_Search_Lucene_Field::Keyword, z. B. Kategorie
Abbildung 9.1 Ein Zend_Search_Lucene-Index besteht aus mehreren Dokumenten, die jeweils mehrere Felder enthalten. Die Daten in manchen Feldern werden nicht gespeichert, und bei manchen Feldern dienen die Daten eher der Darstellung als der Suchfunktion..
Der Index wird dann bei Abfragen durchsucht und ein sortiertes Ergebnisarray (jeweils vom Typ Zend_Search_Lucene_Search_QueryHit) zurückgegeben. Der erste Teil der Implementierung einer Lösung ist bei Zend_Search_Lucene die Erstellung eines Index. Was ist eine Volltextsuchmaschine? Eine solche Suchmaschine durchsucht eine separate Indexdatei der Inhalte einer Website, damit der User das Gewünschte findet. Das bedeutet, dass die Inhaltsinformationen sehr effizient gespeichert werden können, weil sie nur benötigt werden, um den URL zur erforderlichen Seite zu finden (oder die Wege, wie man den URL herausfinden kann). Die Suchmaschine kann die Suchergebnisse auch basierend auf komplexeren Algorithmen sortieren als jenen, die bei einer einfachen Datenbankabfrage zum Einsatz kommen. Das bedeutet hoffentlich, dass für den User die relevanteren Resultate ganz oben ausgegeben werden. Eine Konsequenz ist, dass der gesamte Inhalt, der durchsucht werden soll, sich in der Indexdatei befinden muss, oder er wird nicht gefunden. Wenn auf Ihrer Website neben Datenbankinhalten auch statisches HTML enthalten ist, muss dieses auch in die Indexdatei der Suchmaschine aufgenommen werden..
9.2.1
Einen separaten Suchindex für Ihre Website erstellen
Der Index von Zend_Search_Lucene wird über den Aufruf der Methode create() erstellt: $index = Zend_Search_Lucene::create('Pfad/zum/Index');
Der erstellte Index ist eigentlich ein Verzeichnis, das einige Dateien in einem Format enthält, das mit dem Projekt Apache Lucene kompatibel ist. Das bedeutet, dass Sie auf Wunsch Indexdateien mit Java- oder .Net-Applikationen erstellen können oder umgekehrt, dass Sie Indizes mit Zend_Search_Lucene erstellen und mit Java durchsuchen können.
201
9 Suchfunktionen Nach Erstellung des Index muss dieser nun auch Daten bekommen, die durchsucht werden können. Hier wird’s nun kompliziert, und Aufpassen ist angesagt! Daten können ganz einfach mit der Methode addDocument() eingefügt werden, doch man muss die Felder innerhalb des Dokuments korrekt einrichten, und jedes Feld hat einen bestimmten Typ. Hier folgt ein Beispiel: $doc = new Zend_Search_Lucene_Document(); $doc->addField(Zend_Search_Lucene_Field::UnIndexed('url', $url)); $doc->addField(Zend_Search_Lucene_Field::UnStored('contents', $contents)); $doc->addField(Zend_Search_Lucene_Field::Text('desc', $desc)); $index->addDocument($doc);
Wie Sie in diesem Beispiel sehen können, sind die URL-Daten vom Feldtyp UnIndexed, die Inhalte sind UnStored und die Beschreibung ist Text. Die Suchmaschine behandelt jeden Feldtyp anders, weil sie bestimmen muss, ob die Daten gespeichert oder zur Erstellung der Indexdatei benötigt werden. Tabelle 9.1 zeigt die wesentlichen Feldtypen und deren Unterschiede. Tabelle 9.1 Lucene-Feldtypen zum Hinzufügen von Feldern in einen Index
202
Name
Indexiert
Gespeichert
In Token aufgeteilt
Beschreibung
Keyword
Ja
Ja
Nein
Zum Speichern und Indexieren von Daten, die zum Suchen bereits in separate Wörter getrennt sind.
UnIndexed
Nein
Ja
Nein
Für Daten, in denen nicht gesucht wird, die aber in den Suchergebnissen dargestellt werden, z. B. Datumsangaben oder ID-Felder in Datenbanken.
Text
Ja
Ja
Ja
Für die Speicherung von Daten, die sowohl indexiert als auch zur Darstellung in den Suchergebnissen verwendet werden. Diese Daten werden zum Indexieren in separate Wörter aufgeteilt.
Unstored
Ja
Nein
Ja
Zur Indexierung der Hauptinhalts des Dokuments. Die eigentlichen Daten werden nicht gespeichert, weil sie nicht zur Darstellung der Suchergebnisse verwendet werden.
Binary
Nein
Ja
Nein
Für die Speicherung von Binärdaten, die mit dem Dokument verknüpft sind ( z. B. Thumbnails).
9.2 Die Komponente Zend_Search_Lucene Es gibt zwei Gründe, warum man ein Feld in den Suchindex aufnimmt: Entweder enthält es Suchdaten oder Inhalte, die mit den Resultaten dargestellt werden. Die in Tabelle 9.1 aufgeführten Datenfeldtypen decken diese beiden Operationen ab, und die Wahl des korrekten Feldtyps für eine bestimmte Information ist für den korrekten Betrieb der Suchmaschine ganz wesentlich. Schauen wir uns zuerst die für die Suche gespeicherten Daten an. Diesen Vorgang nennt man Indexierung, und dafür sind die Felder Keyword, Text und Unstored relevant. Der Hauptunterschied zwischen Keyword und Text/Unstored ist das Konzept der Aufteilung in Tokens (tokenizing), also wenn die Index-Engine die Daten analysiert, um die tatsächlich verwendeten Wörter zu bestimmen. Das Feld Keyword wird nicht in Tokens aufgeteilt, was bedeutet, dass jedes Wort zum Zwecke der Suche genauso verwendet wird, wie es geschrieben wurde. Die Felder Text und Unstored werden in Tokens aufgeteilt; also wird jedes Wort analysiert und das zugrundeliegende Basiswort dann für die Suche nach Treffern verwendet. Beispielsweise werden Satzzeichen und Pluralformen aus jedem Wort entfernt. Bei der Arbeit mit einer Suchmaschine muss die Ergebnisliste für die User genug Informationen enthalten, um bestimmen zu können, ob ein bestimmtes Resultat auch das Gesuchte ist. Die gespeicherten Feldtypen können dabei helfen, weil sie von der Suchmaschine zurückgegeben werden. Das sind die Feldtypen Keyword, UnIndexed, Text und Binary. Die Feldtypen UnIndexed und Binary existieren nur zum Zweck der Speicherung von Daten, die zur Darstellung der Ergebnisse verwendet werden. Normalerweise wird der Feldtyp Binary zur Speicherung eines Thumbnail-Vorschaubildes eingesetzt, das sich auf den Eintrag bezieht, und UnIndexed wird für solche Elemente wie eine Zusammenfassung der Ergebnisse oder der Daten verwendet, die mit dem Finden des Resultats zusammenhängen, beispielsweise die Datenbanktabelle, ID oder URL. Zend_Search_Lucene wird den Suchindex automatisch aktualisieren, sobald neue Dokumente aufgenommen werden. Sie können die Optimierung bei Bedarf auch durch Aufruf der Methode optimize() auslösen.
Nun können wir nach Erstellung einer Indexdatei Suchläufe durch den Index ausführen. Wie zu erwarten, enthält Zend_Search_Lucene eine Reihe von leistungsfähigen Mechanismen zur Erstellung von Abfragen, die zu den gewünschten Ergebnissen führen. Das ist unser nächstes Thema.
9.2.2
Leistungsfähige Abfragen
Das Durchsuchen eines Zend_Search_Lucene-Index ist sehr einfach: $index = Zend_Search_Lucene::open(Pfad/zum/Index'); $index->find($query);
Beim Parameter kann es sich um einen String handeln oder Sie erstellen eine Abfrage anhand von Zend_Search_Lucene-Objekten und übergeben eine Instanz von Zend_Search_ Lucene_Search_Query.
203
9 Suchfunktionen Natürlich ist einfacher, einen String zu übergeben, doch für maximale Flexibilität kann der Einsatz eines Zend_Search_Lucene_Search_Query-Objekts sehr nützlich sein. Der String wird über einen Abfrageparser in ein Suchabfrageobjekt konvertiert. Dabei ist es eine gute Faustregel, den Abfrageparser für von Usern angegebene Daten zu nehmen und die Abfrageobjekte direkt zu verwenden, wenn über das Programm Abfragen erstellt werden. Das impliziert, dass man für ein fortschrittliches Suchsystem, das die meisten Websites vorhalten, ein hybrider Ansatz verwendet wird. Das werden wir später untersuchen. Zunächst schauen wir uns den Abfrageparser für Strings an. 9.2.2.1
String-Abfragen
Alle Suchmaschinen enthalten für ihre User ein ganz einfaches Such-Interface: ein simples Textfeld. Somit sind sie sehr leicht zu verwenden, doch auf den ersten Blick scheint es, dass durch dieses Vorgehen komplexere Abfragen schwerer zu formulieren sind. Wie Google hat auch Zend_Search_Lucene einen Abfrageparser, der die Eingabe in einem einfachen Textfeld in eine leistungsfähige Abfrage konvertieren kann. Wenn Sie einen String an die find()-Methode übergeben, wird hinter den Kulissen die Methode Zend_ Search_Lucene_Search_QueryParser::parse() aufgerufen. Diese Klasse implementiert die von Apache Lucene 2.0 unterstützte Lucene-Abfrageparsersyntax. Die Aufgabe des Parsers ist, die Abfrage auf Begriffe, Phrasen und Operatoren herunterzubrechen. Ein Begriff ist ein einzelnes Wort, und eine Phrase sind mehrere Wörter, die in Anführungszeichen gesetzt werden, z. B. „Hello World“. Ein Operator ist ein Boolesches Wort (wie z. B. AND) oder Symbol, mit dem komplexere Suchabfragen formuliert werden können. Mit den Symbolen Sternchen (*) und Fragezeichen (?) werden auch Platzhalter unterstützt. Ein Fragezeichen repräsentiert ein einzelnes Zeichen, und das Sternchen (der „Asterisk“) steht für mehrere Zeichen. Die Suche nach Frame* findet beispielsweise Frame, Framework, Frameset etc. In Tabelle 9.2 finden Sie die wichtigsten Modifikationssymbole, die man auf Suchbegriffe anwenden kann.
204
Modifikator
Symbol
Beispiel
Beschreibung
Wildcards
? und *
H?t h*t
? steht als Platzhalter für ein einzelnes Zeichen und * für mehrere.
Nähe
~x
„php power“~10
Die Begriffe dürfen nur eine bestimmte Wortzahl voneinander entfernt stehen (im Beispiel 10).
Inklusive Bereichsabfragen
Feldname: [x TO y]
category: [skiing TO surfing]
Findet alle Dokumente, deren Feldwerte sich zwischen einem unteren und oberen Grenzwert befinden.
Exklusive Bereichsabfragen
Feldname: {x To y}
published: [20070101 TO 20080101]
Findet alle Dokumente, deren Feldwerte größer als die angegebene Untergrenze, aber kleiner als die angegebene Obergrenze sind. Zu den gefundenen Feldwerten gehören nicht die Grenzwerte selbst.
9.2 Die Komponente Zend_Search_Lucene Modifikator
Symbol
Beispiel
Beschreibung
Boost-Faktor
^x
„Rob Allen“^3
Erhöht die Relevanz eines Dokuments, das diesen Begriff enthält. Die Zahl bestimmt, wie sehr die Relevanz „geboostet“ werden soll.
Tabelle 9.2 Modifikatoren für Suchbegriffe steuern, wie der Parser den Suchbegriff einsetzt. Modifikator
Symbol
Beispiel
Beschreibung
Wildcards
? und *
H?t h*t
? steht als Platzhalter für ein einzelnes Zeichen und * für mehrere.
Nähe
~x
„php power“~10
Die Begriffe dürfen nur eine bestimmte Wortzahl voneinander entfernt stehen (im Beispiel 10).
Inklusive Bereichsabfragen
Feldname: [x TO y]
category: [skiing TO surfing]
Findet alle Dokumente, deren Feldwerte sich zwischen einem unteren und oberen Grenzwert befinden.
Exklusive Bereichsabfragen
Feldname: {x To y}
published: [20070101 TO 20080101]
Findet alle Dokumente, deren Feldwerte größer als die angegebene Untergrenze, aber kleiner als die angegebene Obergrenze sind. Zu den gefundenen Feldwerten gehören nicht die Grenzwerte selbst.
Boost-Faktor
^x
„Rob Allen“^3
Erhöht die Relevanz eines Dokuments, das diesen Begriff enthält. Die Zahl bestimmt, wie sehr die Relevanz „geboostet“ werden soll.
Jeder Modifikator in Tabelle 9.2 wirkt sich nur auf den Begriff aus, an den er angehängt ist. Operatoren hingegen wirken sich auf den Aufbau der Abfrage aus. Die wichtigen Operatoren führt die Tabelle 9.3 auf. Tabelle 9.3 Boolesche Operatoren zur Verfeinerung einer Suche Operator
Symbol
Beispiel
Beschreibung
Erforderlich
+
+php power
Der Begriff nach dem Symbol + muss im Dokument vorkommen.
Ausgeschlossen
-
php -html
Dokumente, in denen der Begriff nach dem Symbol - vorkommt, werden von den Suchergebnissen ausgeschlossen.
AND
AND oder && php and power
Beide Begriffe müssen irgendwo im Dokument stehen.
OR
OR oder ||
php or html
Einer der beiden Begriffe muss in allen zurückgegebenen Dokumenten erscheinen.
NOT
NOT oder !
php not java
Scheidet Dokumente aus, die den Begriff nach NOT enthalten.
205
9 Suchfunktionen Mit einem durchdachten Einsatz der Booleschen Operatoren werden sehr komplexe Abfragen möglich, doch normalerweise werden nur wenige User mehr als einen oder zwei Operatoren in ihren Abfragen einsetzen. Die unterstützten Booleschen Operatoren sind im Prinzip die gleichen, wie sie von den großen Suchmaschinen wie Google eingesetzt werden. Also sind Ihre User zu einem gewissen Grad damit vertraut. Wir schauen uns als Nächstes die andere Möglichkeit an, eine Suchabfrage zu erstellen: die über die Programmschnittstelle. 9.2.2.2
Erstellung von Suchabfragen durch eine API
Zu einer API-Abfrage gehört, die korrekten Objekte zu instanziieren und sie zusammenzusetzen. Es gibt eine ganze Reihe unterschiedlicher Objekte, die man für eine Abfrage kombinieren kann. Tabelle 9.4 listet diese Suchabfrageobjekte auf. Tabelle 9.4 Suchabfrageobjekte Typ
Beispiel und Beschreibung
Begriffsabfrage
Bei einer Begriffsabfrage wird ein einzelner Begriff gesucht. Beispielsuche: category:php $term = new Zend_Search_Lucene_Index_Term('php', 'category'); $query = new Zend_Search_Lucene_Search_Query_Term($term); $hits = $index->find($query); findet den Begriff „php“ im Feld category. Wenn der zweite Begriff des Zend_Search_Lucene_Index_Term-Konstruktors weggelassen wird, werden alle Felder durchsucht.
Mehrfache Begriffsabfrage
Durch Mehrfachbegriffsabfragen können mehrere Begriffe gesucht werden. Für jeden Begriff können Sie optional den Modifikator für erforderlich oder ausschließen anwenden. Alle Begriffe werden bei der Suche auf jedes Dokument angewendet. Beispielsuche: +php power -java $query = new Zend_Search_Lucene_Search_Query_MultiTerm(); $query->addTerm(new Zend_Search_Lucene_Index_Term('php'), true); $query->addTerm(new Zend_Search_Lucene_Index_Term('power'), null); $query->addTerm(new Zend_Search_Lucene_Index_Term('java'), false); $hits = $index->find($query); Der zweite Parameter steuert den Modifikator: true für erforderlich, false für ausschließen und null für kein Modifikator. In diesem Beispiel wird die Abfrage alle Dokument finden, die das Wort „php“ enthalten, vielleicht das Wort „power“, aber nicht das „Wort „java“.
206
9.2 Die Komponente Zend_Search_Lucene Typ
Beispiel und Beschreibung
WildcardAbfragen
Mit Wildcard-Abfragen können Begriffe gesucht werden, bei denen nur einige der Buchstaben bekannt sind. Beispielsuche: super* $term = new Zend_Search_Lucene_Index_Term('super*'); $query = new Zend_Search_Lucene_Search_Query_Wildcard($term); $hits = $index->find($query); Wie Sie sehen, wird das Standard-Zend_Search_Lucene_Index_TermObjekt verwendet, dann an ein Zend_Search_Lucene_Search_Query_Wildcard-Objekt übergeben, damit sichergestellt ist, dass der Modifikator * (oder ?) korrekt interpretiert wird. Wenn Sie einen WildcardBegriff in eine Instanz von Zend_Search_Lucene_Search_Query_Term übergeben, wird er wie ein Literal behandelt.
Phrasenabfragen
Mit Phrasenabfragen kann man nach einer aus mehreren Worten bestehenden Phrase suchen. Beispielsuche: „php is powerful“ $query = new Zend_Search_Lucene_Search_Query_Phrase(array('php', 'is', 'powerful')); $hits = $index->find($query); oder // Separate Index_Term-Objekte $query = new Zend_Search_Lucene_Search_Query_Phrase(); $query->addTerm(new Zend_Search_Lucene_Index_Term('php')); $query->addTerm(new Zend_Search_Lucene_Index_Term('is')); $query->addTerm(new Zend_Search_Lucene_Index_Term('powerful')); $hits = $index->find($query); Die Wörter, aus denen die Abfrage besteht, können entweder im Konstruktor des Zend_Search_Lucene_Search_Query_Phrase-Objekts angegeben werden oder jeder Begriff wird separat hinzugefügt. Sie können auch mit Wort-Wildcards arbeiten: $query = new Zend_Search_Lucene_Search_Query_Phrase(array('php', 'powerful'), array(0, 2)); $hits = $index->find($query); Dieses Beispiel wird alle drei wörtlichen Phrasen finden, bei denen „php“ als erstes und „powerful“ als letztes Wort steht. Das bezeichnet man als „Slop„; Sie können den gleichen Effekt auch mit der setSlop()-Methode erzielen: $query = new Zend_Search_Lucene_Search_Query_Phrase(array('php', 'powerful'); $query->setSlop(2); $hits = $index->find($query);
207
9 Suchfunktionen Typ
Beispiel und Beschreibung
Bereichsabfragen
Bereichsabfragen finden alle Einträge innerhalb eines bestimmten Bereichs, meist ein Datumsbereich, aber es kann sich auch um etwas anderes handeln. Beispielsuche: published_date:[20070101 to 20080101] $from = new Zend_Search_Lucene_Index_Term('20070101', 'published_date'); $to = new Zend_Search_Lucene_Index_Term('20080101', 'published_date'); $query = new Zend_Search_Lucene_Search_Query_Range($from, $to, true /* inklusive */); $hits = $index->find($query); Jede Grenze kann null sein, womit „von Anfang an“ oder „bis zum Ende“ impliziert ist (was jeweils passend ist).
Der Vorteil der API-Nutzung von Zend_Search_Lucene über den String-Parser ist, dass man Suchkriterien einfacher ausdrücken kann, und Ihre User können auf ein erweitertes Formular für die Websuche zugreifen, um ihre Suchabfragen zu verfeinern. Somit haben Sie einen guten Eindruck von Zend_Search_Lucene bekommen und wissen, um welch leistungsfähiges Tool es sich dabei handelt. Bevor es nun darum geht, wie man die Suche auf einer Website implementiert, schauen wir uns an, wie das Optimum aus Zend_Search_Lucene herauszuholen ist.
9.2.3
Best Practices
Wir haben hier schon alles vorgestellt, was Sie für die Arbeit mit Zend_Search_Lucene wissen müssen, doch es ist auch sehr hilfreich, sich ein paar Best Practices anzuschauen. Zunächst einmal sollten Sie als Namen für die Dokumentenfelder nicht id oder score nehmen, weil sie dann schwerer auszulesen sind. Für andere Feldnamen können Sie Folgendes machen: $hits = $index->find($query); foreach ($hits as $hit) { // 'title'-Dokumentfeld holen $title = $hit->title; }
Aber um ein Feld namens id auszulesen, müssten Sie es so schreiben: $id = $hit->getDocument()->id;
Das ist nur bei den Feldnamen id und score erforderlich. Also ist es am besten, wenn Sie mit anderen Namen wie doc_id und doc_score arbeiten. Zweitens müssen Sie die Speichernutzung im Blick haben. Zend_Search_Lucene benötigt eine ganze Menge Speicher! Es wird mehr Speicher gebraucht, wenn Sie viele einmalige Begriffe haben, was vorkommt, wenn Sie eine Menge Phrasen als Feldwerte nehmen, die nicht in Tokens aufgeteilt sind. Das bedeutet, dass Sie große Mengen nicht-textlicher Daten indexieren. Vom praktischen Standpunkt her bedeutet das, dass Zend_Search_Lucene
208
9.3 Eine Suchfunktion für Places am besten bei Textsuchen arbeitet, was für die meisten Websites kein Problem darstellt. Beachten Sie, dass die Indexierung auch ein wenig mehr Speicher braucht, was über den Parameter MaxBufferedDocs gesteuert werden kann. Weitere Einzelheiten finden Sie im Manual des Zend Frameworks. Schließlich arbeitet Zend_Search_Lucene intern mit der UTF-8-Zeichenkodierung, und wenn Sie Nicht-ASCII-Daten indexieren, ist es klug, die Kodierung der Datenfelder anzugeben, wenn sie in den Index eingefügt werden. Das wird über den optionalen dritten Parameter der Methoden zur Felderstellung erledigt. Hier folgt ein Beispiel: $doc = new Zend_Search_Lucene_Document(); $doc->addField(Zend_Search_Lucene_Field::Text('body', $body, 'iso-8859-1'));
Als Nächstes integrieren wir die Suchfunktion mit Zend_Search_Lucene auf der PlacesWebsite.
9.3
Eine Suchfunktion für Places Weil es sich bei Places um eine Community-Site handelt, wird von deren Mitgliedern eine Suchmöglichkeit erwartet. Wir werden eine einfache Suchfunktion mit nur einem Feld auf jeder Seite in der Seitenleiste implementieren, damit die User das Gewünschte ganz einfach finden. Die Suchergebnisse werden so wie in Abbildung 9.2 dargestellt. Dafür müssen wir zuerst die von der Suchfunktion verwendeten Indexdateien erstellen und dann das einfache Formular schreiben sowie die Ergebnisseite durchsuchen und schließlich mit dem erweiterten Suchformular abschließen.
Abbildung 9.2 Die Seite mit den Suchergebnissen von Places: Jeder Eintrag trägt einen Titel, der mit der indexierten Seite verknüpft ist, gefolgt von einer Zusammenfassung. Die Ergebnisse werden ihrem Rang nach geordnet, die wichtigsten stehen ganz oben.
209
9 Suchfunktionen
9.3.1
Indexaktualisierung bei neu eingefügten Inhalten
Uns stehen zwei Möglichkeiten bei der Erstellung von Indexdateien zur Auswahl: Entweder werden die Daten gleich beim Einfügen in die Datenbank indexiert oder über cron oder ein anderes Scheduler-Programm als zeitgesteuerte Aufgabe. Der Traffic der Website bestimmt weitgehend, wie viel Last diese beiden Methoden zusätzlich verursachen. Bei Places werden wir beide Methoden nehmen. Dass neue Einträge in den Index aufgenommen werden, ist für eine nützliche Website unbedingt erforderlich. Allerdings soll es auch möglich sein, alle Daten auf einen Rutsch erneut zu indexieren, damit wir den Suchindex optimieren können, wenn die Datenmenge der Site größer wird. Wir beginnen, indem wir uns erst einmal mit dem Einfügen in den Index beschäftigen, weil uns dieses Wissen auch bei der kompletten Neuindexierung hilfreich sein wird. 9.3.1.1
Die Gestaltung des Index
Wir müssen überlegen, welche Felder wir im Index erstellen wollen. Der Suchindex wird Einträge mit verschiedenen Inhaltstypen enthalten, z. B. Orte, Rezensionen oder die Daten von Benutzerprofilen, doch die Resultate werden dem User zum Lesen als Liste von Webseiten dargestellt. Wir müssen den Satz an Feldern im Index vereinheitlichen, damit die Ergebnisse für den User sinnvoll sind. In Tabelle 9.5 steht der Satz Felder, den wir nehmen werden. Der Basiscode zur Erstellung des zu indexierenden Zend_Search_Lucene_Documents ist der gleiche, egal welche Art von Daten indexiert wird. Er sieht etwa wie der Code in Listing 9.1 aus. Tabelle 9.5 Lucene-Feldtypen zum Hinzufügen von Feldern in einen Index
210
Feldname
Typ
Anmerkungen
class
UnIndexed
Der Klassenname der gespeicherten Daten. Wir brauchen dies beim Auslesen, um einen URL auf die korrekte Seite in der Ergebnisliste zu erstellen.
key
UnIndexed
Der Schlüssel der gespeicherten Daten. Normalerweise ist das die ID des Dateneintrags. Wir brauchen dies beim Auslesen, um einen URL auf die korrekte Seite in der Ergebnisliste zu erstellen.
docRef
Keyword
Ein eindeutiger Identifikator für diesen Eintrag. Damit finden wir den Eintrag für Updates oder Löschungen.
title
Text
Der Titel der Daten. Das werden wir durchsuchen und in den Resultaten darstellen.
contents
UnStored
Der Hauptinhalt für die Suche. Wird nicht dargestellt.
summary
UnIndexed
Die Zusammenfassung, die Informationen über das in den Resultaten dargestellte Suchergebnis enthält. Wird nicht zur Suche benutzt.
9.3 Eine Suchfunktion für Places Feldname
Typ
Anmerkungen
createdBy
Text
Der Autor des Eintrags. Wird zur Suche und Darstellung verwendet. Wir arbeiten mit dem Typ Keyword, damit für die Suche der Name des Autors exakt bewahrt wird.
dateCreatedKeyword
Das erstellte Datum. Wird zur Suche und Darstellung verwendet. Wir verwenden den Typ Keyword, weil Lucene die Daten nicht parsen soll.
Diese Methode erstellt ein Zend_Search_Lucene-Dokument namens $doc. Anschließend fügen wir die Daten aus jedem Feld ins Dokument ein und geben dabei Typ und Namen an n, bevor addDocument() aufgerufen wird o. Weil wir Dokumente für jede Seite der Website einfügen werden, sollten wir die Erstellung des Dokuments so einfach wie möglich gestalten. Wir erweitern dafür Zend_Search_Lucene_Document, damit wir das Objekt mit den richtigen Daten in nur einer Codezeile instanziieren können. Dies sehen Sie in Listing 9.2. Listing 9.2 Erweiterung von Zend_Search_Lucene_Document für einfachere Erstellung class Places_Search_Lucene_Document extends Zend_Search_Lucene_Document { public function __construct($class, $key, $title, $contents, $summary, $createdBy, $dateCreated)
Diese Klasse verfügt über einen Konstruktor, der einfach alle Daten in die Felder einfügt, die wir erstellen wollen. Die neue Places_Search_Lucene_Document-Klasse ist extrem einfach zu verwenden (siehe Listing 9.3). Listing 9.3 Einfügen in den Index $index = Zend_Search_Lucene::open($path); $doc = new Places_Search_Lucene_Document( $class, $key, $title, $contents, $summary, $createdBy, $dateCreated); $index->addDocument($doc);
Index öffnen
Mit einer Codezeile Dokument erstellen Zum Index hinzufügen
Wir müssen diesen Code nun in jede Model-Klasse schreiben, die durchsucht werden soll, und stoßen sofort auf eine Hürde im Design! Das Model kennt eindeutig alles über die zu durchsuchenden Daten, doch es sollte nicht wissen, wie man diese Daten in den Suchindex einfügt. Wäre das der Fall, dann wäre es eng ans Suchsystem gekoppelt, was unser Leben deutlich erschweren würde, falls wir mal auf eine andere Suchmaschine umsatteln wollen. Zum Glück mussten schon vor uns Programmierer mit diesem Problem fertig werden, und das kam so häufig vor, dass ein Designpattern nach der Lösung benannt wurde: das Observer-Pattern. 9.3.1.2
Entkopplung der Indexierung vom Model durch das ObserverPattern
Das Observer-Designpattern beschreibt eine Lösung, die mit dem Konzept der Benachrichtigungen arbeitet. Ein Observer-Objekt meldet Interesse an einem beobachtbaren (observable) Objekt, und wenn mit diesem Objekt etwas passiert, wird der Observer benachrichtigt und kann entsprechende Aktionen auslösen. Unseren Fall zeigt Abbildung 9.3.
212
9.3 Eine Suchfunktion für Places Places-Model (Observable: Instanz von Places_Db_Table_Row_Observable) Benachrichtigt seine Observer, falls etwas passiert
SearchIndexer registriert in Places
SearchIndexer (Observer)
Beim Einfügen benachrichtigt Places den SearchIndexer
Empfängt Benachrichtigung von Observable, dass etwas geschehen ist.
SearchIndexer liest zu durchsuchenden Inhalt aus Places aus
Aktualisiert Suchindexdateien
Abbildung 9.3 Mit dem Observer-Designpattern kann die Suchindexierung von den Model-Daten entkoppelt werden, wodurch es einfacher wird, neue zu durchsuchende Models einzufügen oder die Art zu ändern, wie für die Suche indexiert wird.
Die Observable-Klassen sind unsere Models, also werden wir erlauben, dass Observer sich bei den Models registrieren können. Die Daten, an denen wir interessiert sind, sind Instanzen von Zend_Db_Table_Row_Abstract. Also werden wir eine Erweiterungsklasse namens Places_Db_Table_Row_Observable erstellen, in der die Methoden zur Registrierung und Benachrichtigung der Observer enthalten sind. In Listing 9.4 sehen Sie das Grundgerüst der Klasse. Listing 9.4 Die Klasse Places_Db_Table_Row_Observable class Places_Db_Table_Row_Observable extends Zend_Db_Table_Row_Abstract { protected static $_observers = array();
Erstellt Array
public static function attachObserver($class) mit registrierten { Observern if (!is_string($class) || !class_exists($class) Achtet darauf, dass || !is_callable(array($class, Observer die observeTableRow()'observeTableRow'))) { Funktion enthält return false; } if (!isset(self::$_observers[$class])) { self::$_observers[$class] = true; }
Nimmt Observer in Liste auf, falls er noch nicht drin ist
return true; } protected function _notifyObservers($event) { if (!empty(self::$_observers)) { foreach (array_keys(self::$_observers) as $observer) { call_user_func(array($observer , 'observeTableRow'), $event, $this); } } parent::_postInsert(); }
}
213
9 Suchfunktionen Die ersten beiden Methoden, die wir brauchen, sind der Kern des Observer-Patterns. Mit der Methode attachObserver() kann ein Observer sich selbst anhängen. Wir nehmen eine statische Methode, weil die Liste der Observer von der jeweiligen Model-Klasse unabhängig ist. Entsprechend ist der Array der Observer statisch, weil er von jeder Model-Klasse aus erreichbar sein muss. Die _notifyObservers()-Methode benachrichtigt alle Observer, die an die Klasse angehängt worden sind. Diese Methode iteriert durch die Liste und ruft die statische Methode observeTableRow() innerhalb der Observer-Klasse auf n. Der Observer kann diese Information darüber, worum es sich bei dem Ereignis handelt, und über die Daten aus dem Model nutzen, um die jeweils erforderliche Action auszuführen. In diesem Fall werden wir den Suchindex aktualisieren. In Zend_Db_Table_Row_Abstract sind eine Reihe von Hook-Methoden enthalten, mit denen wir Verarbeitungsschritte vor und nach dem Einfügen, Aktualisieren oder Löschen von Datenbankzeilen ausführen können. Wir nehmen zum Aufruf von _notifyObservers() die Methoden _postInsert(), _postUpdate() und _postDelete(), damit der Suchindex aktualisiert wird. Jede Methode ist die gleiche, und _postInsert() steht in Listing 9.5 (außer dem Benachrichtigungsstring). Listing 9.5 Den Observer nach einer Einfügung informieren class Places_Db_Table_Row_Observable extends Zend_Db_Table_Row_Abstract { protected function _postInsert() Übergibt Ereignistyp als { String an Observer $this->_notifyObservers('post-insert'); } }
Wenden wir uns nun der Observer-Klasse zu. Diese Klasse heißt SearchIndexer, und weil es sich um ein Model handelt, wird sie in application/models gespeichert. Der Code, um sie in der Observable-Klasse zu registrieren, steht in der Bootstrap-Klasse in application/bootstrap.php und sieht wie folgt aus: SearchIndexer::setIndexDirectory(ROOT_DIR . '/var/search_index'); Places_Db_Table_Row_Observable::attachObserver('SearchIndexer');
Wie Sie sehen, geht’s einfach nur darum, das Verzeichnis anzugeben, wo die Suchindexdateien gespeichert werden, und die Klasse über den Namen der Klasse an die Liste der Observer in Places_Db_Table_Row_Observable anzuhängen. Die drei Hauptmethoden in SearchIndexer stehen in den Listings 9.6, 9.7 und 9.8.
214
9.3 Eine Suchfunktion für Places Listing 9.6 Die Hook-Methode zur Benachrichtigung: SearchIndexer::observeTableRow() class SearchIndexer { public static function observeTableRow($event, $row) { switch ($event) { case 'post-insert': Fügt nur bei bestimmten case 'post-update': Ereignissen in Index ein $doc = self::getDocument($row); Liest Daten if ($doc !== false) { aus Model aus self::_addToIndex($doc); } Aktualisiert Suchindex break; } }
Die observeTableRow()-Hook-Methode zur Benachrichtigung prüft den Ereignistyp, und wenn das Ereignis darauf hinweist, dass neue Daten in die Datenbank geschrieben wurden, werden diese aus dem Model ausgelesen n und der Suchindex aktualisiert o. Nun müssen die Daten nur noch aus dem Model ausgelesen werden. 9.3.1.3
Daten des Models in den Index aufnehmen
Der Prozess, die Daten aus dem Model auszulesen, ist eine separate Methode, weil sie verwendet wird, wenn die gesamte Datenbank neu indexiert wird. Diese Methode sehen Sie in Listing 9.7. Listing 9.7 Auslesen der Feldinformation: SearchIndexer::getDocument() class SearchIndexer { // ... public static function getDocument($row) Achtet darauf, { dass Methoden zum if(method_exists($row, Daten auslesen existieren 'getSearchIndexFields')) { $fields = $row->getSearchIndexFields($row); $doc = new Places_Search_Lucene_Document( Holt Daten $fields['class'], $fields['key'], aus Model $fields['title'], $fields['contents'], $fields['summary'], $fields['createdBy'], $fields['dateCreated']); Erstellt neues return $doc; Zend_Search_ } return false; Lucene-Dokument
} // ...
Der SearchIndexer interessiert sich nur für Models, die durchsucht werden müssen. Weil dieses Beobachtungssystem für viele Observer verwendet werden kann, ist es möglich, dass manche Models Benachrichtigungen versenden, die für den SearchIndexer nicht
215
9 Suchfunktionen relevant sind. Um das zu prüfen, checkt getDocument(), ob das Model die Methode getSearchIndexFields() implementiert n. Ist das der Fall, rufen wir die Methode auf, um die Daten aus dem Model in einem Format auszulesen, das für unseren Suchindex passt o. Anschließend erstellen wir das Dokument, das gleich in den Index eingefügt werden kann p. Wir müssen nun ein Dokument in den Suchindex aufnehmen. Das erledigt _addToIndex() (siehe Listing 9.8). Listing 9.8 Einfügen in den Suchindex: SearchIndexer::_addToIndex() class SearchIndexer { // ... protected static function _addToIndex($doc) { $dir = self::$_indexDirectory; $index = Zend_Search_Lucene::open($dir); $index->addDocument($doc); $index->commit();
Öffnet den Suchindex
Fügt Dokument hinzu und
speichert Index auf Festplatte
}
Das Suchdokument in den Index in _addToIndex() aufzunehmen, ist wirklich einfach. Wir brauchen nur den Index zu öffnen und nehmen dazu das im Bootstrap eingerichtete Verzeichnis n. Anschließend wird das Dokument hinzugefügt o. Beachten Sie, dass der Index committet werden muss, damit er auch wirklich für die weiteren Suchläufe gespeichert wird. Wenn Sie viele Dokumente einfügen, ist das nicht erforderlich, weil es ein automatisches Commit-System gibt, das Ihnen diese Arbeit abnimmt. Ein Problem ist, dass Zend_Search_Lucene die Aktualisierung eines Dokuments nicht beherrscht. Wenn Sie ein Dokument aktualisieren wollen, müssen Sie es löschen und erneut einfügen. Wir wollen nicht, dass das gleiche Dokument im Index jemals zweimal vorkommt. Also erstellen wir die Klasse Places_Search_Lucene als Erweiterung von Zend_Search_Lucene und überschreiben addDocument(), damit bei Bedarf zuerst gelöscht wird. Der Code für Places_Search_Lucene steht in Listing 9.9.
216
9.3 Eine Suchfunktion für Places Listing 9.9 Ein Dokument vor dem Einfügen löschen class Places_Search_Lucene extends Zend_Search_Lucene { public function addDocument(Zend_Search_Lucene_Document $document) { $docRef = $document->docRef;
Liest eindeutige docRef aus Dokument ausLiest eindeutige docRef aus Dokument aus
$term = new Zend_Search_Lucene_Index_Term( $docRef, 'docRef'); $query = new Zend_Search_Lucene_Search_Query_Term($term); $results = $this->find($query); if(count($results) > 0) { foreach($results as $result) { $this->delete($result->id); } }
Findet das Dokument
Löscht aktuelle Dokumente aus Index
return parent::addDocument($document);
Fügt neues Dokument in Index ein
}
Diese Methode macht genau, was wir brauchen, aber sie funktioniert nicht! Das liegt daran, dass die statische Methode Zend_Search_Lucene::open() eine Instanz von Zend_Search_Lucene erstellt und nicht von Places_Search_Lucene. Wir müssen die Methoden open() und create() wie in Listing 9.10 gezeigt überschreiben. Listing 9.10 open() überschreiben, damit alles funktioniert class Places_Search_Lucene extends Zend_Search_Lucene { public static function create($directory) { return new Zend_Search_Lucene_Proxy( new Places_Search_Lucene($directory, true)); }
Instanziiert eine Instanz
public static function open($directory) von Places_Search_Lucene { return new Zend_Search_Lucene_Proxy( new Places_Search_Lucene($directory, false)); } // continue class...
Die Methoden create() und open() in Listing 9.10 sind sehr einfach, aber auch dringend erforderlich. Wir müssen SearchIndexer::_addToIndex() aktualisieren, damit wie in Listing 9.11 gezeigt auf Places_Search_Lucene referenziert wird, und alles läuft wie erwartet.
Die Methode in Listing 9.11 ist die gleiche wie in 9.8, außer dass Places_Search_Lucene::open()n aufgerufen wird. Das bedeutet, dass der Aufruf von addDocument()o nun die neu geschriebene Methode aufruft, damit wir sicher sein können, dass es im Suchindex keine doppelten Seiten gibt. Nun haben wir ein funktionierendes System, um den Inhalt des Suchindex aktualisieren zu können. Jetzt brauchen wir nur noch das Suchsystem im Frontend zu implementieren, damit unsere User das Gesuchte finden. 9.3.1.4
Neuindexierung der gesamten Site
Da wir nun über die Möglichkeit verfügen, den Index zu aktualisieren, wenn neue Inhalte aufgenommen werden, können wir den bereits geschriebenen Code einsetzen, um die gesamten Daten der Site erneut zu indexieren. Das ist sehr praktisch, um einen Massenimport von Daten (Bulk Insert) zu ermöglichen, und auch als Recovery-Strategie, falls der Index beschädigt oder versehentlich gelöscht wurde. Am einfachsten nimmt man die Neuindexierung über die Erstellung einer ControllerAction vor: search/reindex (siehe Listing 9.12). Listing 9.12 Der Controller für die Neuindexierung public function reindexAction() { $index = Places_Search_Lucene::create( SearchIndexer::getIndexDirectory()); $places = new Places(); $allPlaces = $places->fetchAll(); foreach($allPlaces as $place) { $doc = SearchIndexer::getDocument($place); $index->addDocument($doc); }
Erstellt neuen Index, löscht alten
Fügt der Reihe nach jedes Dokument hinzu
}
Die reindex-Action ist sehr einfach. Sie arbeitet mit der create()-Methode von Zend_Search_Lucene, um einen neuen Index zu beginnen, wodurch ein vorhandener Index de facto überschrieben wird. Um alle Dokumente aufzunehmen, nutzen wir die getDocument()-Methode des SearchIndexers , die ein Dokument anhand der Methode getSearchIndexFields() des Models erstellt, dabei den Code erneut verwendet und diese Methode sehr einfach macht.
218
9.3 Eine Suchfunktion für Places TIPP
Die Neuindexierung sollte über Zend_Acl geschützt werden, wenn sie auf einem Live-Server deployt wird. Ein User mit bösen Absichten könnte nämlich diese Action wiederholt aufrufen, was zu einer beträchtlichen CPU-Last führte und die Website wahrscheinlich zum Stillstand brächte.
Wir haben nun eine umfassende Kontrolle über die Erstellung von Suchindizes auf der Places-Website und können uns mit der Erstellung eines Suchformulars beschäftigen, damit die User auf der Site suchen und sich anschließend die Resultate anschauen können.
9.3.2
Erstellen des Suchformulars und Darstellung der Ergebnisse
Das Suchformular für Places ist sehr einfach. Es besteht nur aus einem einfachen Texteingabefeld und einer Go-Schaltfläche. Das Formular steht auf jeder Seite zur Verfügung, und das HTML dafür ist also in views/layouts/_search.phtml enthalten (siehe Listing 9.13). Listing 9.13 Ein einfaches Suchformular in HTML
Search:
Erstellt Go-Button
zum Auslösen der Suche
Das Action-Attribut des Suchformulars zeigt auf die Index-Action im Such-Controller, wo die Suche stattfindet. Weil Places das MVC-Pattern befolgt, wird der Suchlauf in der Methode SearchController::indexAction() vorgenommen, und die Darstellung der Ergebnisse wird in die damit verknüpfte View-Datei views/scripts/search/index.phtml separiert. Schauen wir uns zuerst den Controller an. 9.3.2.1
Verarbeitung einer Suchanfrage im Controller
Diese Methode führt die Suche durch und weist die Ergebnisse der View zu. Sie validiert und filtert außerdem die Eingaben des Users, um zu gewährleisten, dass sich nicht unbeabsichtigt XSS-Sicherheitslecks einschleichen können. Die Controller-Action steht in Listing 9.14.
219
9 Suchfunktionen Listing 9.14 Filtern und Validieren für das Suchformular public function indexAction() { $this->view->title = 'Search Results';
Da wir mit Daten arbeiten, die User eingegeben haben, muss erst einmal gewährleistet sein, dass diese auch sicher sind. Die Komponente Zend_Filter_Input kümmert sich sowohl ums Filtern als auch ums Validieren. Mit dem Filtern entfernen wir jegliche Ausfütterung des Suchbegriffs mit Leerraumzeichen und mit dem StripTags-Filter auch das ganze HTML n. Beim Suchbegriff nehmen wir nur eine Validierung vor, um sicher zu sein, dass der User etwas eingegeben hat o, da die Suche nach einem leeren String keine nützlichen Resultate zurückgeben wird! Die isValid()-Methode von Zend_Filter_Input filtert die Daten und prüft, ob die Validierung bestanden wurde p. Bei Erfolg übernehmen wir den Text der Suchanfrage und weisen ihn zur Darstellung der View zu. Nach Prüfung, ob die vom User eingegebenen Daten zur Verwendung in Ordnung sind, können wir nun die Suche durchführen. Wie immer bei Zend_Search_Lucene öffnen wir zuerst den Index q und rufen dann die Methode auf, die die Arbeit ausführt r. In diesem Fall können wir den integrierten String-Abfrageparser einsetzen, weil der User eine sehr einfache Suchabfrage angeben kann (wie z. B. „zoo“, um alle Zoos zu finden, oder eine kompliziertere Abfrage wie „warwickshire -zoo“, um alle Sehenswürdigkeiten in Warwickshire außer Zoos zu finden). Wenn die Validierung fehlschlägt, holen wir den Grund dafür aus Zend_Filter_Input, indem wir den Rückgabewert von getMessages()s der View zuweisen. Nachdem wir nun entweder einen Ergebnissatz generiert haben oder die Validierung fehlgeschlagen ist, müssen wir dem User diese Informationen in der View ausgeben.
220
9.3 Eine Suchfunktion für Places 9.3.2.2
Die Suchergebnisse in der View darstellen
Die View ist für zwei Dinge verantwortlich: dem User etwaige Fehlermeldungen und die Suchergebnisse darzustellen. Für die Fehlermeldungen iterieren wir einfach durch die Liste und geben sie dann in einer Liste aus. Dies sehen Sie in Listing 9.15. Listing 9.15 Darstellung der Fehlermeldungen von Zend_Filter_Input messages) : ?>
Setzt id zum Stylen There was a problem: des Outputs mit CSS
messages['q'] as $msg) : ?>
Iteriert über alle Nachrichten für „q“
Das erklärt sich von selbst, und es bleibt nur noch anzumerken, dass wir nur über das qArray in messages iterieren, weil wir wissen, dass es in diesem Formular nur ein Formularfeld gibt. Bei einem komplizierteren Suchformular müssten wir durch alle Formularfelder iterieren. Die zweite Hälfte des View-Skripts stellt die Suchergebnisse dar. Die uns zur Verfügung stehenden Felder sind auf jene begrenzt, die wir in Places_Search_Lucene_Document eingerichtet haben (in Listing 9.2), und wir verwenden diese Felder im Output (siehe Listing 9.16). Listing 9.16 Darstellung der Fehlermeldungen von Zend_Filter_Input
You searched for escape($this->q); ?>. results);?> results found. Verwendet escape() zum
Wie bei jeder Reaktion auf eine User-Action geben wir ein wichtiges Feedback darüber, wonach der User gesucht hat und wie viele Treffer gefunden wurden. Dann wird durch das Ergebnis-Array iteriert, und alle Informationen über jedes Element werden in einer unsortierten Liste dargestellt. Die Suchergebnisse enthalten nicht den URL zu der Seite, die das Resultat enthält. Das müssen wir also den Feldern class und key entnehmen, die ja im Suchindex stehen. Das wird an die View-Hilfsklasse getSearchResultUrl() delegiert
221
9 Suchfunktionen (siehe Listing 9.17), die den Code enthalten soll. Die Ergebnisse werden durch das Feld score sortiert, das die Gewichtung aller Treffer anzeigt. Der User ist daran nicht interessiert, aber wir vielleicht; es wird als Kommentar eingebunden, damit es über den Befehl Quellcode anschauen untersucht werden kann, wenn Suchabfragen überprüft werden. Natürlich kann das bei einer produktiven Applikation weggelassen werden. Listing 9.17 View-Hilfsklasse zum Auslesen des URLs des Suchergebnisses function getSearchResultUrl($class, $id) { Achtet darauf, dass $id = (int)$id; Parameter „vernünftig“ sind $class = strtolower($class); $url = $this->_view->url(array('controller'=>$class, 'action'=>'index', 'id'=>$id)); return $url; }
Die anfängliche Version von getSearchResultUrl() ist sehr einfach, weil es darin eine 1:1-Zuordnung vom Klassennamen des Models zur Controller-Action gibt. Das heißt, bei einem Model namens Places ist der verwendete Controller places/index. Das wird sich wahrscheinlich ändern, wenn weitere Models in die Applikation aufgenommen werden. Wenn dies passiert, wird die Zuordnung vom Model zum URL komplexer und vollständig in die View-Hilfsklasse aufgenommen. Somit wird die Wartung und Pflege auf lange Sicht deutlich vereinfacht.
9.4
Zusammenfassung Dieses Kapitel stellte eine der außergewöhnlichen Komponenten des Zend Frameworks vor. Zend_Search_Lucene ist eine sehr umfassende Volltextsuchmaschine, die komplett in PHP geschrieben ist. Damit können Entwickler Suchfunktionen auf einer Website integrieren. Wir beschäftigten uns eingehend mit der Art, wie Zend_Search_Lucene funktioniert und wie Suchabfragen entweder aus einem einfachen String bestehen können wie bei einer Google-Suche oder über eine API konstruiert werden, um sehr komplexe Abfragen zu ermöglichen. Um Zend_Search_Lucene in einen Kontext zu stellen, nahmen wir die Suche auf der Places-Community-Website auf. Die Suche in Places ist relativ simpel gestrickt, weil sie nur ein Model enthält, das indexiert werden muss. Allerdings machen wir den Code zukunftssicher, weil wir mit dem Observer-Pattern die Suchindexierung von den Models separieren, wo die Daten gespeichert sind. Das Ergebnis ist eine Suchmaschine, die einen Suchalgorithmus mit Rankingfunktion ausführt und den Usern dabei hilft, schnell die gewünschten Informationen zu finden. Das bringt Ihrer Website natürlich verschiedene Vorteile.
222
10 10 E-Mails Die Themen dieses Kapitels
Die Arbeit mit Zend_Mail E-Mail-Versand mit sendmail und SMTP Integration von Zend_Mail in eine Zend Framework-Applikation Erstellung von E-Mails im HTML-Format Lesen von E-Mails PHP enthält eine angemessene Bandbreite von Funktionen für E-Mails: von der mail()Funktion, die den meisten PHP-Programmierern jeglicher Erfahrungsstufe vertraut sein dürfte, über IMAP, POP3 bis hin zu NNTP-Funktionen. Bedauerlicherweise müssen Letztere in der PHP-Installation speziell einkompiliert werden, und das bedeutet, dass sie für manche User nicht verfügbar sind. Im Kontrast dazu ist Zend_Mail eine ziemlich vollständige Mail-Implementierung, bei der neben den allgemeinen Anforderungen des Zend Frameworks selbst keine spezielle Konfiguration anfällt. Wir beginnen dieses Kapitel mit allgemeinen Informationen darüber, wie E-Mails funktionieren, und beschäftigen uns anschließend mit der Konstruktion einer E-Mail-Nachricht über Zend_Mail. Von dort erweitern wir das praktische Wissen durch den Aufbau eines einfachen Support-Trackers, den wir zur Entwicklung der Places-Applikation einsetzen.
10.1 Die Grundlagen von E-Mails Immer, wenn wir unseren Kunden eine bestimmte Technologie erklären wollen, machen wir uns auf die Suche nach vergleichbaren Beispielen aus der realen Welt. Das fällt insbesondere dann leicht, wenn man E-Mails erklären will, weil diese tatsächlich der normalen „Schneckenpost“ nachgebildet sind. Schauen wir uns an, wie E-Mails funktionieren und welchen Teil Zend_Mail bei diesem Vorgang spielt.
223
10 E-Mails
10.1.1 E-Mails – einfach dargestellt So wie der komplexe Weiterleitungsprozess bei der normalen Briefpost einfach darauf reduziert werden kann, dass ein Brief in den Briefkasten geworfen wird und man dann darauf wartet, dass er beim Empfänger im Briefkasten landet, geht das auch bei den EMails, allerdings um ein Vielfaches schneller. Im folgenden Beispiel (siehe Abbildung 10.1) schauen wir uns die ganz typische Nutzung von E-Mails in einer Webapplikation an. Dabei schickt unser User Bert eine Einladung an seinen Freund Ernie, damit dieser sich auf einer fantastischen neuen Website registriert, die Bert gerade gefunden hat. Bert beginnt, indem er in das Formular der Website seine Nachricht schreibt, und wenn er fertig ist, klickt er auf die Schaltfläche zum Abschicken. Die Inhalte werden dann an den Server in einer POST- (oder gelegentlich auch mal mit einer GET-) HTTP-Request-Methode gesendet. Zend_Mail wandelt diese Informationen dann ins E-Mail-Format um und leitet sie an den Mail Transfer Agent (MTA) weiter. Weil Zend_Mail standardmäßig mit Zend_Mail_Transport_Sendmail arbeitet, der ein Wrapper für die mail()-Funktion von PHP ist, ist sendmail der zu erwartende Mail Transfer Agent. Nach Empfang der E-Mail leitet der MTA alle lokalen Mails an lokale Mailboxen weiter (d. h. für Domänen mit einem DNS-Eintrag auf dem gleichen Rechner) oder (im Falle dieser E-Mail für Ernie) platziert sie in eine Warteschlange zur Weiterleitung. Wenn die Warteschlange abgearbeitet und versendet ist, wandert die E-Mail von einem Server zum nächsten und landet schließlich beim Mailserver des Empfängers, wo sie auf ihre Abholung wartet. Ab hier muss Ernie nur noch auf „Neue Nachrichten holen“ klicken, und sein Mail-Client (Mail User Agent, MUA) sammelt die Post über ein Protokoll wie POP3 (Post Office Protocol) ein. Mail-Client des Empfängers (MUA)
HTML-Formular des Senders ---- ------- -- ----- ------- ---- - --- --$_POST oder $_GET
Abbildung 10.1 Eine vereinfachte Darstellung des Vorgangs, wie eine E-Mail aus einem HTMLFormular an den E-Mail-Client des Empfängers gesendet wird.
224
10.2 Die Arbeit mit Zend_Mail
10.1.2 Analyse einer E-Mail-Adresse Damit wir uns die Weiterleitung (das sogenannte Routing) der E-Mail-Nachrichten detaillierter anschauen können, nehmen wir uns die zentrale Komponente vor: die E-MailAdresse. Am einfachsten vergleicht man das mit einer physischen Adresse. In Tabelle 10.1 werden beide nebeneinander gestellt. Während Berts physischer Standort durch eine immer breiter (oder enger – je nach dem, wie Sie es lesen) werdende geografische Beschreibung gekennzeichnet ist, nutzt seine E-Mail-Adresse entsprechend eine Serie von Suffixen, um seinen Standort im Netzwerk zu identifizieren. Tabelle 10.1 Vergleich einer physischen Adresse mit einer E-Mail-Adresse Physische Adresse
E-Mail-Adresse
Name des Empfängers:
Bert
Kontoname
bert@
Postadresse
10 Some Street,
Kontodomäne
bertsblog
Generische Top-Level-Domäne
.com
Sydney, NSW 2000
Ländercode-Top-Level-Domäne (als Ergänzung und optional) Land
au
Australien
So wie die Weiterleitung der physischen Post von den sendenden Bestandteilen der verschiedenen postalischen Mechanismen und Postmitarbeiter verantwortet wird, kümmern sich auch die sendenden MTAs um den Transfer der E-Mail. Durch ständige Bezugnahme auf das Domain Name System (DNS) arbeiten sich die MTAs rückwärts durch die E-MailAdresse, bis die Nachrichten an der lokalen Domäne eintrifft. Auf jeder Stufe wird der eingeschlagene Pfad von der Verfügbarkeit der Server und der erfolgreichen Überwindung von Firewalls, Spam- sowie Virenfiltern bestimmt. Über die Arbeitsweise von E-Mails gibt es noch viel mehr zu sagen, doch hier soll es für den Einstieg genügen, und wir schauen uns das an, was uns am meisten interessiert: Zend_Mail. Wir werden uns mit einigen seiner Features beschäftigen und konzentrieren uns auf bestimmte Details von E-Mails, um die Zend_Mail sich kümmern kann.
10.2 Die Arbeit mit Zend_Mail Während bei obigen Ausführungen Zend_Mail so erscheint, dass es eine recht kleine Rolle im Gesamtprozess hat, ist diese nichtsdestotrotz recht ausschlaggebend. Zend_Mail setzt man nicht nur beim Versand von Mails per sendmail oder SMTP ein, sondern damit können Sie auch Nachrichten zusammenstellen, Attachments anhängen, HTML-Mails versenden und sogar Mail-Nachrichten lesen.
225
10 E-Mails
10.2.1 E-Mails mit Zend_Mail erstellen Eine E-Mail braucht mindestens drei Dinge, bevor sie per Zend_Mail versendet werden kann:
einen Sender, einen Empfänger und einen Nachrichtenteil (Body). In Listing 10.1 sehen Sie, wie dieses knappe Minimum plus Betreffzeile völlig ausreicht, damit Bert seine Einladung an Ernie schicken kann. Alle, die mit der mail()-Funktion von PHP vertraut sind, werden sofort zu schätzen wissen, wie Zend_Mail die MailZusammenstellung in einem viel angenehmeren Interface kapselt. Listing 10.1 Ein einfaches Zend_Mail-Beispiel setFrom('[email protected]', 'Support'); $mail->addTo('[email protected]', 'Bert'); $mail->setSubject('An Invite to a great new site!'); $mail->setBodyText( 'Hi Bert, Here is an invitation to a great new web site.' ); $mail->send();
Wenn wir nun einen Blick auf die Zusammensetzung der E-Mail werfen, fällt zuerst ins Auge, dass die aus Listing 10.1 produzierte E-Mail einfach genug ist, um der E-MailSpezifikation (auch als RFC 2822 bekannt) zu genügen. Die Spezifikationen für E-Mails werden vom offiziellen Internet-Gremium der IETF (Internet Engineering Task Force) formuliert, und es lohnt, sich mit diesen Informationen vertraut zu machen, weil überall darauf verwiesen wird. Das Zend Framework-Manual besagt beispielsweise, dass die Validierungsklasse Zend_Validate_EmailAddress „jede valide, mit dem RFC 2822 konforme E-Mail-Adresse erkennen wird“. Leider handelt es sich beim RFC 2822 um eine recht limitierte Spezifikation, und wenn Bert Einladungen an Freunde in nicht englischsprachigen Ländern hätte schicken wollen, dann hätte er ohne MIME ein Problem. MIME (Multipurpose Internet Mail Extensions) ist ein Sammelname für eine Reihe von RFC-Dokumenten, die die Basisfunktionen von EMails um weitere Funktionen ergänzen, z. B. Zeichenkodierung, Attachments etc. Zend_Mime stellt in Zend_Mail die MIME-Funktionalität bereit; sie kann auch für sich genommen als Komponente verwendet werden. In Wirklichkeit kümmert sich der ganze Code in Listing 10.1 darum, dass ein paar Kopfzeilen definiert werden, dann folgt der eigentliche Nachrichtentext, und dann wird die EMail versendet. In Tabelle 10.2 vergleichen wir die Rolle dieses E-Mail-Inhalts mit den Bestandteilen eines echten Briefs.
226
10.2 Die Arbeit mit Zend_Mail Tabelle 10.2 Vergleich von Brief und E-Mail Brief
E-Mail
Umschlag
Empfänger und Adresse
Kopfzeilen
Sender- und Empfängername und Anderes
Brief
Die geschriebenen Inhalte
Body
Der Text der zu versendenden Nachricht
So wie Sie Bilder Ihrer Kinder in einem Briefumschlag stecken, können Sie mit Zend_Mail auch Bilder als Anhänge (Attachments) versenden, indem Sie etwa eine solche Zeile schreiben: $mail->createAttachment($bildDerKinder);
Wenn Sie dieses wichtige Bild in den Umschlag gesteckt haben, können Sie eine Warnung draufschreiben: „Fotos – Bitte nicht knicken!“ Das geht theoretisch auch mit Zend_Mail, indem wir weitere Kopfzeilen einfügen: $mail->addHeader('PhotoWarningNote', 'Fotos – Bitte nicht knicken!');
Natürlich wird die Anmerkung auf dem Umschlag wahrscheinlich ignoriert – genauso wie die Kopfzeile, weil PhotoWarningNote kein Header ist, der von einem Mail User Agent erkannt wird. Das Einfügen von Headern in die mail()-Funktion von PHP ist etwas, worum sich ein Entwickler kümmern muss, weil es böswilligen Usern möglich ist, Header wie z. B. Adressen von weiteren Empfängern zu ergänzen. Das wird als E-Mail-Header-Injection bezeichnet, aber zu Ihrer Erleichterung sollen Sie wissen, dass Zend_Mail alle Header filtert, um einen solchen Angriff zu verhindern. Da wir nun wissen, wie eine E-Mail erstellt wird, können wir uns um die beim Versand möglichen Optionen kümmern.
10.2.2 E-Mails mit Zend_Mail versenden Bei Zend_Mail können Mails entweder mit sendmail oder per SMTP versendet werden. Schauen wir uns an, wann Sie einen dieser Wege dem anderen vorziehen sollten und welche Optionen jeweils zur Verfügung stehen. 10.2.2.1 Versand per sendmail Es wurde bereits in diesem Kapitel erwähnt, dass Zend_Mail standardmäßig mit Zend_ Mail_Transport_Sendmail arbeitet, das wiederum die mail()-Funktion von PHP benutzt. Das bedeutet, dass Ihre E-Mails einfach erstellt und an die mail()-Funktion von PHP übergeben und von dort an den lokalen sendmail-Mailserver (oder dessen Entsprechung) weitergeleitet werden, der für den eigentlichen Transfer der E-Mail sorgt, falls Sie sich nicht anders entscheiden.
227
10 E-Mails Um das etwas deutlicher zu beschreiben, nutzen wir die Option von PHP-mail(), um in den an den Mailserver gesandten Befehlen zusätzliche Parameter zu übergeben. In diesem Fall nutzen wir es, um wie folgt einen Header im Konstruktor von Zend_Mail_Transport_Sendmail einzurichten: $transportWithHeader = new Zend_Mail_Transport_Sendmail( '[email protected]' ); Zend_Mail::setDefaultTransport($transportWithHeader);
Damit wird [email protected] als vierter Parameter der mail()-Funktion übergeben. mail() sendet dann diesen Parameter im folgenden sendmail-Befehl, der die Versenderadresse der E-Mail setzt: sendmail -f [email protected]
Weil wir bloß einen Befehl mitsenden, sollte diese Methode schnell sein und nur wenig Latenz verursachen, doch das hängt davon ab, wie die Rechner eingerichtet sind, von denen sie aufgerufen und gestartet wird. Anmerkung
Weil es sich bei um einen *nix-Befehl handelt, ist das für Windows-Server keine Option, weil diese standardmäßig sowieso mit SMTP arbeiten.
10.2.2.2 Versand per SMTP Es gibt Gelegenheiten, bei denen Sie den lokalen Mailserver nicht mit dem Versand Ihrer Mails belasten wollen. Ein gutes Beispiel wäre, wenn viele Mails in einer Umgebung versendet werden sollen, wo es Mengenbeschränkungen für Ihren lokalen Mailserver gibt. Ein weiteres Beispiel wäre, wenn Mails von einem Web-Cluster verschickt werden sollen, die den gleichen Ausgangsserver haben. Dann verhindert es, dass Ihre E-Mails als Spam kategorisiert werden, wenn sie im Postfach der Empfänger landen. In diesem Fall ist es möglich, die Mails per Simple Mail Transfer Protocol (SMTP) an einen anderen Serviceprovider zu übergeben. Weil SMTP der Standard ist, über den alle Mails im Internet versendet werden, werden unsere E-Mails letzten Endes doch mit SMTP verschickt, auch wenn wir mit sendmail arbeiten. Wenn es hier also um den Versand per SMTP geht, ist gemeint, dass Zend_Mail so eingerichtet wird, dass E-Mails über einen speziellen SMTP-Ausgangsserver versandt werden. Das geschieht weitgehend auf die gleiche Weise, wie wir unsere E-Mail-Clients für den Versand von Mails einrichten. In Listing 10.2 wird Zend_Mail so eingerichtet, dass der Versand per SMTP erfolgt und dabei die vom Serviceprovider erforderliche Authentifizierung sowie eine sichere Verbindung genutzt wird.
228
10.2 Die Arbeit mit Zend_Mail Listing 10.2 Zend_Mail zur Nutzung einer SMTP-Verbindung einrichten 'tls', 'port' => 25, für sichere Transportschicht 'auth' => 'login', Setzt optionale Authentifizierungs'username' => 'myusername', details für SMTP-Server 'password' => 'mypassword' ); Übergibt Details für Server und $transport = new Zend_Mail_Transport_Smtp( 'mail.our-smtp-server.com', $authDetails Authentifizierung an Konstruktor ); Zend_Mail::setDefaultTransport($transport);
Setzt SMTP als Standardtransportmethode
Nachdem Zend_Mail so eingerichtet ist, dass es über die SMTP-Verbindung Mails sendet, wollen wir das gleich durch Verschicken mehrerer E-Mails testen. 10.2.2.3 Mehrere E-Mails per SMTP versenden Es gibt Gelegenheiten, wo Sie mehrere Mails in einem Rutsch versenden wollen, z. B. einen Newsletter an viele User. Allerdings steht über die mail()-Funktion im PHP-Manual folgende Anmerkung: Bitte beachten Sie, dass die mail()-Funktion nicht dazu geeignet ist, große Mengen von E-Mails in einer Schleife zu senden, da die Funktion für jede E-Mail einen SMTPSocket öffnet und schließt, was nicht sehr effizient ist. http://www.php.net/manual/de/function.mail.php
Im Gegensatz dazu bleibt bei Verwendung von Zend_Mail_Transport_Smtp die SMTPVerbindung standardmäßig geöffnet, bis das Objekt nicht mehr verwendet wird. Von daher ist es zum Versand großer Mengen von E-Mails besser geeignet als mail(). Hier folgt ein Beispiel: foreach ($users as $user) { $mail->addTo($user->email, $user->full_name); $mail->setBodyText('Hi ' . $user->first_name . ', Welcome to our first newsletter.'); $mail->send(); }
Wenn Sie sich fragen, warum jemand überhaupt einen solch uninteressanten Newsletter verschicken sollte, sind Sie bereit für den nächsten Abschnitt. Darin stellen wir die Arbeit von Zend_Mail in einer echten Applikation vor und holen mehr aus dem E-Mail-Body heraus.
229
10 E-Mails
10.3 Einen Support-Tracker für Places erstellen Je länger Sie an einem Projekt arbeiten, desto mehr Listen mit Bugs, Problemen und Ideen für Features kommen da zusammen. Diese Listen neigen dazu, überall verstreut zu sein: Sie stehen in E-Mails, werden nach Telefonaten festgehalten oder unleserlich bei Meetings aufs Papier gekritzelt. Es gibt schon Möglichkeiten, wie man solche Listen pflegen kann, z. B. Bug-Tracker und Applikationen fürs Projektmanagement, doch das ist für Kunden meist überdimensioniert – und nebenbei bemerkt, wäre das auch viel zu einfach! Wir werden stattdessen eine eigene Lösung aufbauen, und weil der Schlüssel zu einem guten Support in der Kommunikation besteht, wird sich unser Support-Tracker vor allem über Mails kundtun. Dieser Support-Tracker soll auch die Zend_Mail-Optionen zum Lesen von Mails demonstrieren.
10.3.1 Die Applikation entwerfen Unser Support-Tracker muss Folgendes können:
Er muss einfach genug sein, dass wir lieber mit ihm als mit einem normalen E-MailClient arbeiten.
Updates für den Status von Bugs müssen möglich sein. Er muss sich in die aktuellen Userdaten integrieren können, sodass keine neuen Usernamen und Passwörter nötig sind.
Er muss alle erforderlichen Adressaten per Mail benachrichtigen können. Dabei müssen auch E-Mail-Anhänge wie Screenshots möglich sein und mit den Benachrichtigungsmails versendet werden können.
Er muss optional formatierte E-Mails erlauben, damit der Leser die Mails besser und schneller überfliegen kann. Aus der zweiten Anforderung ist zu entnehmen, dass wir diese Daten speichern müssen, und die Abbildung 10.2 zeigt die anfängliche Tabellenstruktur unserer Applikation. Weil wir bei einem einzelnen User viele Support-Themen haben könnten, wird das in der oneto-many-Beziehung des Schemas deutlich.
230
10.3 Einen Support-Tracker für Places erstellen support
users
+id: int +user_id: int
+id: int user_id
+date_created: datetime
+type: enum ('bug','feature')
+date_updated: datetime
+priority: int
+username: varchar(100)
+status: enum
+password: varchar(40)
('open','resolved','on_hold')
+title: varchar(100) +body: text +body_formatted: text +date_modified: timestamp
+first_name: varchar(100) +last_name: varchar(100) +date_of_birth: date +sex: char(1) +postcode: varchar(20)
+date_created: datetime
Abbildung 10.2 Die anfängliche Datenbankstruktur des Support-Trackers erfordert das Einfügen einer Support-Tabelle neben der vorhandenen User-Tabelle.
Wenn Sie das Kapitel 5 gelesen haben, sollte Ihnen die Arbeit mit den Zend_Db_TableBeziehungen vertraut sein, und Sie erkennen sicher das Model aus Listing 10.3. Durch Angabe von $_dependentTables und $_referenceMap definieren wir die Beziehung zwischen der vorhandenen Users-Klasse aus der Places-Applikation und der neuen Klasse Support_Table. Listing 10.3 One-to-many-Beziehung mit Zend_Db_Table class Users extends Zend_Db_Table_Abstract { protected $_name = 'users'; protected $_dependentTables = array('Support_Table'); } class Support_Table extends Zend_Db_Table_Abstract { protected $_name = 'support'; protected $_rowClass = 'Support_Row'; protected $_referenceMap = array( 'Support' => array( 'columns' => array('user_id'), 'refTableClass' => 'Users', 'refColumns' => array('id') ) ); }
Die Verlinkung dieser beiden Klassen erfüllt die dritte Anforderung der Integration der aktuellen User-Daten von Places in den Support-Tracker. Die User werden in der Lage sein, Support-Tickets zu übermitteln, ohne sich in ein separates System einloggen zu müssen. Damit Support-Tickets eingefügt werden können, brauchen wir zumindest ein Einreichungsformular und die Funktionalität, einen Eintrag in der Datenbank vornehmen zu können. Wir beginnen mit dem Erstellen einer Support-Model-Klasse (siehe Listing 10.4), die für die Bearbeitung der Daten verantwortlich sein wird.
231
10 E-Mails Listing 10.4 Die Model-Klasse Support include_once 'Support/Table.php'; class Support { public function __construct() { $this->_supportTable = new Support_Table; } public function getIssues() { return $this->_supportTable->fetchAll(); } public function getIssue($id) { $where = $this->_supportTable->getAdapter() ->quoteInto('id = ?', $id); return $this->_supportTable->fetchRow($where); }
Erstellt neues Zend_Db_Table_Row-Objekt, falls keine ID vorhanden
Nimmt vorhandene Datenbankzeile, falls ID vorhanden
Richtet row-Daten ein
Speichert Zeile und gibt row-ID zurück
}
Um einen gewissen Kontext zu bieten, bindet Listing 10.5 andere Methoden ein, die in der Support-Klasse enthalten sind. Weil wir uns in diesem Kapitel auf E-Mails konzentrieren, ist die zentrale Methode der Support-Klasse saveIssue(), weil es sich dabei um jene Action handelt, die die Benachrichtigungs-E-Mails auslösen wird. Bis wir den Mail-Code eingefügt haben, nimmt saveIssue() die Support-Ticket-Daten an, entscheidet, ob es sich um ein neues Ticket oder das Update eines vorhandenen handelt,
232
10.3 Einen Support-Tracker für Places erstellen filtert die Daten und speichert sie in der Datenbank ab. Listing 10.5 zeigt, wie saveIssue() aufgerufen wird, nachdem ein gültiges Support-Ticket in die ControllerActionklasse SupportController übermittelt wurde. Listing 10.5 Die Methode addAction() in der Actionklasse SupportController public function addAction() { $form = new SupportForm('/support/create/'); $this->view->formResponse = ''; if ($this->getRequest()->isPost()) { if ($form->isValid( $this->getRequest()->getPost() )) { Ruft saveIssue() nach $id = $this->_support->saveIssue( gültiger Formular$form->getValues() übermittlung auf ); return $this->_redirect('/support/edit/id/' . $id . '/'); } else { $this->view->formResponse = 'Sorry, there was a problem with your submission. Please check the following:'; } } $this->view->form = $form; }
Wenn Sie Kapitel 8 gelesen haben, werden Sie erkennen, dass wir anhand von Zend_Form das Übermittlungsformular für das Support-Ticket in Listing 10.5 generieren. Es wäre nicht sonderlich hilfreich, den Code hier durchzugehen, doch aus Gründen der Vollständigkeit und als anschauliche Referenz zeigt Abbildung 10.3 das finale gerenderte Formular.
Abbildung 10.3 Das Übermittlungsformular des Support-Trackers, mit dem Bug- oder FeatureAnfragen an das Entwicklungsteam gesandt werden können.
233
10 E-Mails Wir machen nun mit der vierten Anforderung weiter: die Benachrichtigung des SupportTeams, wenn Support-Tickets eingefügt und aktualisiert werden. Das bringt uns auch wieder zurück zum Thema dieses Kapitels: den E-Mails.
10.3.2 Integration von Zend_Mail in die Applikation Ihnen ist sicher schon aufgefallen, dass es bisher im Code noch keine Mail-Funktionalität gibt, und aktuell macht saveIssue() nichts anderes als das Filtern der Eingabe und das Erstellen der Datenbankzeile. Die erste Frage lautet, wo der Code fürs Mailen eingefügt werden soll. Wir wissen, dass die Methode Support::saveIssue() mit Zend_Db_Table_Abstract arbeitet, das letzten Endes über Zend_Db_Table_Row::save() vorhandene Zeilen aktualisiert und auch neue erstellt. Das ist eine mögliche Stelle, um die Mail an das SupportTeam auszulösen. In Listing 10.6 sehen Sie die Inhalte einer möglichen Zend_Db_Table_Row-Unterklasse und wie die save()-Methode die save()-Methode der übergeordneten Klasse überschreiben sowie die Benachrichtigungs-E-Mail an das Support-Team einfügen kann. Listing 10.6 auszulösen
Überschreiben von save() in der Support_Row-Unterklasse, um eine E-Mail
class Support_Row extends Zend_Db_Table_Row_Abstract { Instanziiert Zend_Mail, public function save() Ruft die save()-Methode { richtet Header ein und der Elternklasse auf parent::save(); versendet Mail $mail = new Zend_Mail(); $mail->setBodyText($this->body); $mail->setFrom('[email protected]', 'The System'); $mail->addTo('[email protected]', 'Support Team'); $mail->setSubject(strtoupper( $this->type) . ': ' . $this->title ); $mail->send(); } }
In Listing 10.6 wird, sobald die save()-Methode aufgerufen wird, eine E-Mail an das Support-Team gesendet, in der im Body der Mail Informationen über das Support-Ticket stehen. Es ist kaum übersehbar, dass dieser Code suboptimal ist. Wenn wir beispielsweise später eine Kopie an den Übermittler senden, ein Attachment anhängen oder vielleicht den Nachrichtentext formatieren wollten, würde die save()-Methode über ihren Zweck hinaus aufgebläht. Behalten wir das im Hinterkopf, wenn wir den Mailing-Code aus der save()Methode refakturieren und eine Klasse namens Support_Mailer (siehe Listing 10.7) erstellen, die sich auf diese Aufgabe konzentrieren kann.
234
10.3 Einen Support-Tracker für Places erstellen Listing 10.7 Die Klasse Support_Mailer wird die Benachrichtigungs-E-Mails versenden. include_once 'Table.php'; class Support_Mailer { public $supportId; public function __construct($supportId) { $this->supportId = intval($supportId); }
Übergibt SupportTicket-ID an Konstruktor
function sendMail($html=false) Liest Problem{ Datenbankzeile aus $supportTable = new Support_Table; $supportIssue = $supportTable->find($this->supportId); $mail = new Zend_Mail(); Setzt HTML-Body,
Die Support_Row::save()-Methode in Listing 10.6 könnte nun unsere neue MailingKlasse aufrufen und wie folgt deren id als Parameter übergeben: $id = parent::save(); $mailer = new Support_Mailer ($id); $mailer->sendMail();
Das ist ein wenig unbefriedigend, weil wir nun Mail-Code haben, der in ein Objekt ganz unten in der Ebene für die Datenbearbeitung eingebettet ist. Idealerweise sollten wir die Mail-Funktion komplett von den Datenobjekten entkoppeln. Dafür könnten wir prima das in Kapitel 9 vorgestellte Observer-Designpattern einsetzen. Wenn wir das in diesem Kapitel machen würden, dann würde das vom Hauptthema der E-Mail ablenken. Also nehmen wir den Mittelweg und verschieben den Aufruf von Support_Mailer::sendMail() in die letzten Zeilen der saveIssue()-Methode in der eher allgemeinen Support-Klasse in Listing 10.4: $id = $row->save(); $mailer = new Support_Mailer ($id); $mailer->sendMail(); return $id;
235
10 E-Mails An dieser Stelle wird es später leichter zu refakturieren sein, und wir können uns nun mit der Funktionalität der Mail-Klasse selbst beschäftigen.
10.3.3 Header in der Support-E-Mail einfügen Die vierte Anforderung in der Spezifikation der Applikation war, dass „alle betroffenen Adressaten“ Benachrichtigungs-E-Mails erhalten sollten. Es stellt sich heraus, dass damit nicht nur das Support-Team bei [email protected] gemeint ist. Tatsächlich sollen alle Admin-User des Systems per E-Mail informiert werden, und der Übermittler soll auch eine Kopie bekommen. Wir müssen auch einen Prioritätshinweis einfügen, den E-Mail-Clients wie Microsoft Outlook und Apple Mail erkennen können. Glücklicherweise ist das bei beiden recht einfach und erfordert nur den Einsatz von Headern. 10.3.3.1 Einfügen von Empfängern Aus Gründen der Einfachheit speichert unsere Applikation die Rollen der User als einzelnes Feld in der Tabelle Users. Um also alle Admin-User auszulesen, brauchen wir folgende Abfrage: $users = new Users; $where = array( 'role = ?' => 'admin', 'user_id <> ?' => Zend_Auth::getInstance()->getIdentity()->id ); $rows = $users->fetchAll($where);
Ihnen fällt sicher der zusätzliche Begriff im Argument für fetchAll() auf, der alle Fälle herausfiltert, bei denen der Übermittler gleichzeitig ein Admin-User ist. Das verhindert, dass sie eine doppelte unnötige Admin-E-Mail neben der CC-Version bekommen, die sie als Übermittler sowieso erhalten. In manchen Fällen wollen Sie vielleicht lieber, dass der Übermittler eine andere Version bekommt als die, die an das Support-Team gesendet wird. Also ist das weitgehend eine Frage der Implementierung. Nach Auslesen aller Admin-User können wir nun eine Schleife durchlaufen und diese User der Mail mit einem To-Header einfügen: foreach ($rows as $adminUser) { $mail->addTo($adminUser->email, $adminUser->name); }
Als Nächstes lesen wir anhand der ID von Zend_Auth die E-Mail-Details für den Übermittler aus, und wir fügen sie in der Mail mit einem CC-Header ein: $users->find(Zend_Auth::getInstance()->getIdentity()->id); $submitter = $users->current(); $mail->addCC($submitter->email, $submitter->name);
Auch die Option ist möglich, User mit einem BCC-Header über Zend_Mail::addBcc() einzubinden, aber in diesem Fall brauchen wir das nicht.
236
10.3 Einen Support-Tracker für Places erstellen 10.3.3.2 Header einfügen Über die addHeader()-Methode können zusätzliche Header gesetzt werden, wobei die ersten zwei Argumente ein Paar aus Name und Wert des Headers ist. Ein drittes Boolesches Argument verweist darauf, ob es mehrere Werte für den Header gibt. Die angefragte Prioritätsindikation ist ein zusätzlicher Header, der in der E-Mail als name:value-Paar erscheint. X-Priority: 2
Wir waren schlau genug, diese Anforderung schon vorwegzunehmen, und haben das Datenbankfeld mit einem Integer versehen, der mit dem Wert der Priorität korrespondiert. Dieser Wert kann nun einfach als Header eingefügt werden. Ihnen ist vielleicht die folgende Zeile im Support_Mailer-Code in Listing 10.7 aufgefallen: $mail->addHeader('X-Priority', $supportIssue->current()->priority, false);
Die Argumente spezifizieren nun, dass wir einen Header namens X-Priority einfügen, dass er den Prioritätswert für das aktuelle Support-Ticket enthält und dass es nur einen Wert haben kann. Bei der empfangenen E-Mail kann man nun auf einen Blick die Priorität sehen, wenn der E-Mail-Client des Empfängers den Prioritäts-Header erkennt. Wir haben bereits erwähnt, dass wir die Erstellung der Support_Mailer-Klasse deswegen erstellt haben, weil sie Erweiterungen erlaubt. Ein Beispiel dafür wäre, Attachments an die E-Mail anzuhängen – darum geht es gleich.
10.3.4 Attachments an Support-E-Mails anhängen Man kann mit großer Sicherheit davon ausgehen, dass es keinen einzigen Entwickler gibt, der dieses Buch liest und noch nie Bug-Reports mit so vagen Beschreibungen wie „das ist kaputt“ oder „es funktioniert einfach nicht“ bekommen hat. Das eigentliche Problem einzuengen, kann manchmal frustrierender sein, als es zu beheben. In solchen Fällen kann ein Screenshot der problematischen Seite eine große Hilfe sein, und darum ist das Anfügen von Attachments an die Support-E-Mail die fünfte Anforderung unserer Applikation. Nachdem wir das Übermittlungsformular für das Support-Ticket um ein Datei-einfügenFeld ergänzt haben, kann der User nach der Datei mit dem Screenshot suchen und zusammen mit der Übermittlung des Formulars hochladen. Dem Prozess des Umgangs mit hochgeladenen Dateien können wir in diesem Kapitel nicht gerecht werden, doch nehmen wir an, dass es in etwa wie folgt funktioniert: move_uploaded_file( $_FILES['attachment']['tmp_name'], realpath(getcwd()) . '/support/' . $id . '/' );
Damit wird die hochgeladene Datei, die wir error-screenshot.jpg nennen wollen, in ein Unterverzeichnis von /support/ verschoben und die ID des Support-Tickets als Verzeichnisname genommen.
237
10 E-Mails Nachdem die Datei nun an Ort und Stelle ist, können wir sie an die E-Mail mit dem in Listing 10.8 gezeigten Code anhängen. Listing 10.8 Die Screenshot-Datei an die Support-E-Mail anhängen $file = "/home/places/public_html/support/67/err.jpg"; $fileBody = file_get_contents($file); $fileName = basename($file); $fileType = mime_content_type($file); Hängt Attachment
Die optionalen Einstellungen in Listing 10.8 sind nur erforderlich, wenn Ihr Attachment vom Standard abweicht, also kein binäres Objekt ist, das mit einer Base64-Kodierung transferiert und als Attachment behandelt wird. In unserem Fall haben wir angegeben, dass das Attachment inline in der E-Mail dargestellt werden soll, damit wir die Datei zum Anschauen nicht separat öffnen müssen. Wie bereits in diesem Kapitel erwähnt, kümmert sich Zend_Mime um solche Einstellungen. Wenn Sie also mehr erfahren wollen, schauen Sie sich bitte den entsprechenden Abschnitt im Zend Framework Manual an. Nachdem nun auch die fünfte Anforderung erfüllt ist, bleibt uns nur noch die sechste und letzte, dass die E-Mails formatiert sind, damit sie vom vielbeschäftigten Admin-Team schnell und leicht gelesen werden können.
10.3.5 Formatierung der E-Mail Die Frage, ob man Text- oder HTML-E-Mails versenden sollte, ist ein heiß umstrittenes Thema, das bei fast jeder Gelegenheit gerne aufgetischt wird. Egal wie Ihre persönlichen Vorlieben sind: Als Entwickler müssen Sie einfach wissen, wie beide verschickt werden, obwohl Zend_Mail ironischerweise den Prozess einfacher macht als die Wahl. 10.3.5.1 Versenden von formatierten HTML-E-Mails Als Teil der Eingabefilterung in der saveIssue()-Methode unserer Support-Klasse wird der Nachrichtentext als HTML formatiert, wofür das PHP-Konvertierungstool Markdown von Michel Fortin eingesetzt wird (das auf dem Originalcode von John Gruber beruht). Der formatierte Nachrichtentext wird dann in einem separaten Feld zusammen mit dem Original gespeichert, womit wir zwei Versionen des Bodys in unserer E-Mail nutzen können. Ihnen ist vielleicht in der Support_Mailer-Klasse in Listing 10.7 aufgefallen, dass wir das durch folgenden Code ermöglicht haben: if ($html) { $mail->setBodyHtml($supportIssue->current()->body_formatted); } $mail->setBodyText($supportIssue->current()->body)
238
10.3 Einen Support-Tracker für Places erstellen Als wir diesen Code zuerst im Listing 10.7 einführten, haben wir nicht erwähnt, wo das eingestellt werden sollte. Weil es sich um eine persönliche Präferenz handelt, könnten wir es wie folgt aus der Information des aktuell autorisierten Users wieder herstellen: $mailer->sendMail( Zend_Auth::getInstance()->getIdentity()->mail_format );
Selbst wenn Ihr User sich für den Empfang einer HTML-Version Ihrer E-Mail entschieden hat, können Sie für jene, die das nicht lesen können oder wollen, wie hier gezeigt zusätzlich eine reine Textversion versenden. 10.3.5.2 Formatierung mit Zend_View Wir wissen nun, wie man vorformatierte Mails als Text- oder HTML-Versionen verschicken kann, doch was ist, wenn der Nachrichtenteil schon vor Versand formatiert werden soll? Als wir die Empfänger hinzugefügt haben, ging es schon einmal darum, dass Sie an den Übermittler des Support-Tickets vielleicht eine andere E-Mail verschicken wollen als ans Support-Team. Anhand dieser Übermittler-E-Mail werden wir am Beispiel demonstrieren, wie man mit Zend_View eine E-Mail aus der gleichen Art von View-Skript rendern kann, wie wir es in den HTML-Seiten verwendet haben. Wir haben beschlossen, eine E-Mail an den Übermittler zu versenden, die ihn über die Nummer des Support-Tickets informiert, den übermittelten Text einschließt, eine kurze Beschreibung des weiteren Vorgehens enthält und mit ein paar Dankesworten abschließt. Listing 10.9 zeigt die Textversion. Listing 10.9 Textversion der E-Mail für den Übermittler eines Support-Tickets in text-email.phtml Hello user->name; ?>, Your support ticket number is supportTicket->id; ?> Your message was: supportTicket->body; ?> We will attend to this issue as soon as possible and if we have any further questions will contact you. You will be sent a notification email when this issue is updated. Thanks for helping us improve Places, The Places Support team.
Wenn Sie nicht gerade als Erstes dieses Kapitel aufgeschlagen haben, dann sollte Ihnen das ziemlich bekannt vorkommen, weil es den View-Skripten aus den vorigen Kapiteln recht ähnlich ist. Die HTML-Version in Listing 10.10 ist möglicherweise noch vertrauter. Listing 10.10 HTML-Version der E-Mail für den Übermittler eines Support-Tickets in html-email.phtml
Hello user->name; ?>,
Your support ticket number is supportTicket->id; ?>
239
10 E-Mails
Your message was:
supportTicket->body_formatted; ?>
We will attend to this issue as soon as possible and if we have any further questions will contact you. You will be sent a notification email when this issue is updated.
Thanks for helping us improve Places,
The Places Support team.
Damit wir diese View-Skripte auch in den E-Mails nutzen können, müssen wir sie durch Zend_View wie folgt rendern lassen: $mail->setBodyText($this->view->render('support/text-email.phtml')); $mail->setBodyHtml($this->view->render('support/html-email.phtml'));
Abbildung 10.4 zeigt die so entstandenen E-Mails. Das ist eindeutig sehr praktisch und simpel, vor allem, weil es sich um den gleichen Prozess handelt, der für jedes andere View-Skript auch verwendet wird. Somit hat dieser Vorgang, weil er flexibel genug ist, auch alle möglichen anderen Einsatzzwecke wie z. B. E-Mails wegen vergessener Passwörter bis hin zu umfangreicheren HTML-Newslettern.
Abbildung 10.4 Gegenüberstellung des Outputs der Support-Ticket-Benachrichtung als Text- (links) und HTML-E-Mails (rechts)
Mit der Erfüllung aller Anforderungen unseres Support-Trackers haben wir auch die Zend_Mail-Features für Erstellung und Versand von E-Mails abgedeckt. So bleibt uns noch das letzte Glied in der E-Mail-Kette: das Lesen von Mails. Auch dafür bietet Zend_Mail umsichtigerweise die entsprechende Funktionalität.
240
10.4 Lesen von E-Mails
10.4 Lesen von E-Mails Unser Support-Tracker verfügt nun über ein Webformular, mit dem man Probleme festhalten und das Support-Team informieren kann, doch Probleme können auch aus anderen Quellen kommen, z. B. indem sie per E-Mail gesendet oder weitergeleitet werden. Um auch damit umzugehen, nutzen wir die Fähigkeit von Zend_Mail, Mails lesen zu können. Wir werden ein Support-E-Mail-Konto überwachen und Probleme aus diesen Nachrichten in den Support-Tracker einfügen. Bevor wir uns die Anforderungen für dieses neue Feature in unserer Applikation anschauen, wollen wir uns erst ein paar der Komponenten zum Abholen und Speichern von E-Mails vor Augen führen.
10.4.1 Abholen und Speichern von E-Mails Zu Anfang dieses Kapitels stellten wir fest, dass Ernie Berts Einladung über POP3 abgeholt hat, doch er hätte auch genauso gut IMAP nehmen können, das andere Protokoll zum Abholen von E-Mails. Weil man bei Zend_Mail mit beidem arbeiten kann, schauen wir uns deren Unterschiede einmal an. 10.4.1.1 POP3 und IMAP Unsere Hauptpersonen Bert und Ernie nutzen diese beiden Protokolle, um auf ihre Mails zuzugreifen, und in der Abbildung 10.5 wird gezeigt, worin Berts Nutzung von IMAP sich von Ernies POP3 unterscheidet. Weil Ernie im Homeoffice arbeitet und kaum einmal von woanders auf seine Mails zugreifen muss, arbeitet er mit POP3. Bert ist dauernd unterwegs und muss von überall her an seine Mails kommen, darum verwendet er IMAP. Mail-Client des Empfängers nutzt POP3
Wartet auf Abholung Versand-Mailserver arbeitet mit POP3
E-Mails werden abgeholt und vom Server gelöscht, wenn nichts anderes angegeben ist.
---- ------- --- ----- --- - --- - ----- --
Mail-Client des Empfängers nutzt IMAP
Wartet auf Lesen Versand-Mailserver arbeitet mit IMAP
Mail wird auf dem Server gelesen und bleibt dort, falls nichts anderes angegeben ist.
---- ------- --- ----- --- - --- - ----- --
Abbildung 10.5 Vergleich der Hauptunterschiede bei der E-Mail-Abholung zwischen POP3 (Verbindungsaufbau, Abholung der Mails und Verbindungstrennung) und dem leistungsfähigeren IMAP
POP3 könnte man als das ältere und „dümmere“ der beiden Protokolle bezeichnen. Es baut die Verbindung auf, holt die Mails ab und trennt die Verbindung wieder, wobei der Mail
241
10 E-Mails User Agent (also der E-Mail-Client) den Großteil der Verantwortung hat. Auf der anderen Seite führt die Tatsache, dass es so simpel ist und allgemein unterstützt wird, neben seinem geringen Verbrauch an Serverressourcen dazu, dass es weithin eingesetzt wird. IMAP ist im Vergleich dazu leistungsfähiger und trägt in der Beziehung zum E-MailClient eine größere Verantwortung. Die Mail wird auf dem Server belassen, und der EMail-Client fordert eine Kopie an, die er lokal cachen wird. Anders als POP3 erlaubt IMAP eine permanente Verbindung mit dem Server, den Zugriff mehrerer Clients auf die gleiche Mailbox, E-Mail-Ordner, serverseitige Suchen, partielles Auslesen von Mails etc. Wenn Sie wie Bert von verschiedenen Standorten oder mit unterschiedlichen Geräten (z. B. PDA, Handy oder Laptop) auf Ihre Mails zugreifen wollen, wäre IMAP eine bessere Lösung, weil jedes Gerät einfach auf die Mail zugreift und sie nicht abholt. 10.4.1.2 Speichern von E-Mails Im Gegensatz zu den offiziellen Internetstandards, die wir bisher beschrieben haben, wie z. B. MIME, SMTP, POP3, IMAP und das im RFC 2822 umrissene E-Mail-Format ist das Dateiformat zum Speichern von E-Mails nicht standardisiert. Stattdessen wird das den Entwicklern der E-Mail-Clients überlassen. unterstützt aktuell die Formate Mbox und Maildir, wobei der wesentliche Unterschied zwischen beiden darin besteht, dass Ersteres eine Schreibsperre (file locking) auf Applikationsebene erfordert, um die Integrität der Nachricht zu bewahren. Es bringt an dieser Stelle nicht viel, detailliert auf die Formate einzugehen, also können wir uns wieder an unsere Applikation machen.
Zend_Mail
10.4.2 E-Mails mit der Applikation lesen Zuerst halten wir ein paar Anforderungen für diese zusätzliche Arbeit fest, bevor’s richtig losgeht. Anhand dieser Liste wird deutlich, an was uns gelegen ist und wie Zend_Mail uns dabei helfen kann. Das wären also die Anforderungen an die Applikation, die wir zusammengestellt haben:
Sie sollte regelmäßig Mails von einem bestimmten E-Mail-Konto lesen und in der Support-Tabelle speichern.
Sie sollte Details über den Verfasser des Berichts herausfinden und speichern können. Sie sollte die Original-Mail als Dateianhang speichern können. Bevor wir etwas mit der gespeicherten Mail machen können, müssen wir dazu zunächst einmal eine Verbindung öffnen. 10.4.2.1 Eine Verbindung öffnen Für die Produktionsversion des Support-Trackers ist es am wahrscheinlichsten, dass wir entweder mit Zend_Mail_Storage_Maildir oder (wie im folgenden Beispiel) mit Zend_ Mail_Storage_Mbox eine Verbindung zu einem lokalen Speicherstandort aufbauen:
242
10.4 Lesen von E-Mails $mail = new Zend_Mail_Storage_Mbox( array('filename' => '/home/places/mail/inbox') );
Für unser Beispiel werden wir mit Remote-Speicherung arbeiten, weil das die Lösung ist, auf die die meisten Leser zugreifen können und mit der sie sofort und nur mit minimalem Einrichtungsaufwand arbeiten können. Das machen wir mit Zend_Mail_Storage_Imap oder Zend_Mail_Storage_Pop3. Wir entscheiden uns für Letzteres, weil es überall vorhanden ist, und bauen die Verbindung mit diesem Code auf: $mail = new Zend_Mail_Storage_Pop3(array('host' => 'example.com', 'user' => 'support', 'password' => 'support123'));
Bei unserem Mail-Objekt ist nun eine Verbindung geöffnet, und wir können die Nachrichten abholen. 10.4.2.2 Abholen und Speichern der Support-Nachrichten Weil Zend_Mail_Storage_Abstract eine der Standard-Iterator-Klassen der PHP-Library implementiert, kann leicht darüber iteriert werden. Also beginnen wir mit der ersten Anforderung für diese Applikation und kümmern uns darum, wie die Nachrichten in der Support-Tabelle gespeichert werden. Listing 10.11 zeigt die erste Implementierung ohne Filtern. Listing 10.11 Die Support-E-Mail in eine Zeile der Support-Tabelle umwandeln
Verwendet xPriority-Header foreach ($mail as $messageNum => $message) { als Prioritätseinstellung $data['priority'] = isset($message->xPriority)? $message->xPriority : 3; Verwendet E-Mail-Betreff $data['status'] = 'open'; als Titel des Problems $data['title'] = $message->subject; $data['body'] = $message->getContent(); Verwendet E-Mail-Body $id = $this->saveIssue($data); als Problem-Body }
Kombinieren wir diesen Code mit einer Möglichkeit, wie er periodisch gestartet werden kann (z. B. mit cron), und die erste Anforderung ist praktisch schon erledigt. Ein paar Dinge müssen noch angesprochen werden; beispielsweise haben wir noch nichts an dem body_formatted-Feld der Support-Tabelle gemacht. Als wir die Body-Information mit dem Webformular eingegeben haben, wurde sie durch den Markdown-Filter Text to HTML von Markdown geschickt. In diesem Fall könnte der Body der E-Mail Text oder HTML oder beides sein. (Letzteres nennt man auch Multipart.) Wenn wir einen HTML- oder Multipart-Body direkt in das Feld für den Nachrichtentext eingeben, wird’s total chaotisch. Darum werden wir das in Listing 10.12 nachprüfen und den Body auf den reinen Textinhalt reduzieren.
243
10 E-Mails Listing 10.12 Die Reduzierung einer Multipart-E-Mail auf die reine Textkomponente
Schleife, solange Content-Type-Header von $part Multipart enthält
$part = $message; while ($part->isMultipart()) { $part = $message->getPart(1);
Weist ersten Teil der Multipart-Message erneut $part zu
}
if (strtok($part->contentType, ';') == 'text/plain') { $plainTextContent = $part->getContent(); }
Prüft, ob erster Teil nur Text ist
Weist Textinhalt einer Variablen zu
Theoretisch könnten wir auch jeden HTML-Teil in das body_formatted-Feld der SupportTabelle einfügen, doch dann müssten wir wieder genau auf die Filterung der Daten achten. Und auch wenn wir das gemacht hätten, müssten wir uns wieder mit der ganzen Vielfalt des HTML-Markups der verschiedenen E-Mail-Clients herumschlagen. Stattdessen übergeben wir einfach die Textversion an den Markdown-Filter Text-to-html in Support::saveIssue(), damit er eine einfache, aber saubere Formatierung bekommt. So bleibt uns nur noch die folgende Anpassung und Ergänzung des Codes aus Listing 10.12: $data['body'] = $plainTextContent;
Auch wenn wir eine korrekt formatierte Version des Nachrichtentexts der Support-E-Mail speichern, kann es sein, dass das gemeldete Problem selbst uns nicht alle erforderlichen Informationen bietet. Aus diesem Grund speichern wir auch den Absender der E-Mail, um bei Rückfragen einen Ansprechpartner zu haben. support +id: int +user_id: int +type: enum ('bug','feature') +priority: int +status: enum ('open','resolved','on_hold')
+title: varchar(255) +body: text +body_formatted: text +date_modified: timestamp +date_created: datetime +reported_by: varchar(255)
Abbildung 10.6 Die support-Tabelle mit dem neuen Feld reported_by, über das wir die Einzelheiten über den Absender der Support-E-Mail festhalten können.
In Abbildung 10.6 haben wir in die support-Tabelle das Feld reported_by eingefügt, das diese Information aufnimmt, und wir müssen noch den folgenden Code in die Support::saveIssue()-Methode in Listing 10.4 aufnehmen:
244
10.4 Lesen von E-Mails $row->reported_by = $filterStripTags->filter($data['from']);
Wir müssen auch den Absender in den Code von Listing 10.12 mit dieser Ergänzung aufnehmen: $data['reported_by'] = $message->from;
Wir könnten auch noch etwas Zeit dafür aufwenden, diesen Absender-String in Name und E-Mail-Adresse aufzuteilen, aber für den Moment reicht das erst einmal. Unsere support-Tabelle kann nun per Mail versandte Tickets aufnehmen, doch als letzte Maßnahme werden wir die E-Mail als Dateianhang speichern, falls wir aus welchen Gründen auch immer darauf noch einmal zurückgreifen wollen. 10.4.2.3 Die empfangene Mail als Datei abspeichern Wenn wir die Support-Mail-Nachrichten in Listing 10.12 durchschleifen, greifen wir den gesamten Text aus den Mails ab, den wir brauchen, und speichern ihn in der Variable $message, der dann in eine Datei geschrieben werden kann. Listing 10.13 zeigt den Code dafür, der die dritte Anforderung erfüllt. Listing 10.13 Die Nachricht in eine Datei schreiben
Schleift durch die Header und $messageString = ''; hängt an $messageString an foreach ($message->getHeaders() as $name => $value) { $messageString .= $name . ': ' . $value . "\n"; Hängt Nachrichteninhalt } an $messageString an $messageString .= "\n" . $message->getContent(); file_put_contents( getcwd() . '/support/' . $id . '/email.txt', Speichert in Datei in einem $messageString Verzeichnis, das nach der );
Support-Ticket-ID benannt ist
Diese abschließende Ergänzung bedeutet, dass diese gespeicherten Textdateien nicht nur als praktisches Backup dienen, falls es Probleme mit dem Einfügen in die Datenbank gibt, sondern man kann sie bei Bedarf auch prima an andere aus dem Team weiterleiten. Schauen wir uns nun den vollständigen Code an. 10.4.2.4 Die finale readMail()-Methode In Listing 10.14 bringen wir nun den gesamten bisher geschriebenen Code zusammen, damit Sie sehen, wie alles zusammenpasst. Während Sie ihn durchgehen, sollten Sie beachten, dass die Filterung in Support::saveIssue() für eine gewisse Sicherheit gegenüber bösartigem Code sorgt, der in E-Mails eingebaut sein könnte. Wir arbeiten auch mit TypeHinting, um sicher zu sein, dass die Methode die Mailverbindung übergeben bekommt, die wir zu Beginn dieses Abschnitts aufgebaut haben, höchstwahrscheinlich als ControllerAction.
Wir haben nun alle drei Anforderungen für den Einbau des Support-Trackers erfüllt und konnten die Lesefunktionalität von Zend_Mail auch praktisch demonstrieren.
10.5 Zusammenfassung Nach Lektüre dieses Kapitels werden Sie sicher entdecken, dass E-Mail ein viel tiefer gehendes Thema ist, als Sie je gedacht haben. Weil beinahe alle Webapplikationen in unterschiedlicher Intensität mit E-Mails arbeiten, haben wir versucht, genug Hintergrund zu vermitteln, damit Sie Zend_Mail nicht nur besser verstehen, sondern auch, wie es in den Gesamtzusammenhang passt. Durch Einbau von Zend_Mail in die Komponenten aus den vorigen Kapiteln verfügen wir nun auch über ein gutes Grundlageninstrumentarium zur Erstellung von Webapplikationen. Bevor wir unser Arsenal weiter aufstocken, nehmen wir im nächsten Kapitel zum Thema Deployment einen Umweg über einige Praktiken, mit denen wir die Entwicklung solcher Applikationen verbessern können.
246
11 11 Deployment Die Themen dieses Kapitels
Einen Server für mehrere Websites einrichten Versionskontrolle mit Subversion Funktionale Teststrategien mit Selenium IDE und Zend_Http_Client Mit PHP und Zend Framework sind Webapplikation sehr schnell entwickelt, aber dies kann einen auch dazu verführen, Abkürzungen bei der Entwicklung zu nehmen. Viele Leser werden schon einmal diesen kalten Schweiß gespürt haben, wenn man aus Versehen einen wichtigen Codeabschnitt gelöscht hat und dann wie ein Irrer versucht, eine Live-Site zu fixen, bevor jemand etwas merkt! In diesem Kapitel gehen wir Entwicklungs- und Deployment-Methoden durch, mit denen man die Qualität der Arbeit sicherstellen kann, ohne dass die eigene Begeisterung für die Arbeit mit dem Framework gedämpft wird. Durch ein solches Sicherheitsnetz bekommen Sie durch die von uns ausgeführten Praktiken mehr Freiheit und Vertrauen in die Art, wie Sie Ihre Arbeit anpacken.
11.1 Den Server einrichten Als Ausgangspunkt dieses Kapitels schauen wir uns die Umgebung an, unter der die Entwicklung bis zum finalen Deployment fortschreitet. So wie Übungs- und Probedurchläufe bei einer Theateraufführung dazu gedacht sind, Probleme in der Live-Produktion zu verhindern, können Probleme in den Live-Implementierungen durch eine sorgfältige Vorbereitung und korrektes Einrichten der Serverumgebung vermieden werden. Typische Umgebungen für Development, Staging und Production werden wie folgt eingerichtet (unabhängig davon, ob sie sich auf der gleichen physischen Maschine befinden oder auf mehrere verteilt sind):
247
11 Deployment
Development: Dies ist die Probephase, in der die Anwendung entwickelt wird. Hier werden Änderungen vorgenommen und getestet, und die finale Produktion wird geformt. Abhängig von den Vorlieben des jeweiligen Entwicklers wird das auf einer oder auf mehreren Maschinen durchgeführt. Unterschiedliche Konfigurationen auf den Entwicklungsmaschinen haben außerdem den Vorteil, dass Bugs offensichtlich werden, die anderenfalls durchgerutscht wären.
Staging: Wie eine Kostümprobe sollte in dieser Phase die finale Produktionsumgebung nachgestellt werden, und zwar so genau wie möglich. Auf diesem Server finden keine Entwicklungsarbeiten statt, sondern alle Veränderungen müssen weiterhin auf dem Development-Server vorgenommen und dann auf den Staging-Server geschoben werden. Auf diese Version kann nur vom Entwicklungsteam oder eventuell anderen Personen zugegriffen werden, die mit der Moderation des Releases der Applikation zu tun haben.
Production: Dies ist der Live-Server für die öffentliche Vorführung, der über das Internet zugänglich ist. Auf diesem Server findet keine Entwicklung statt, weil damit nicht nur möglicherweise eine Live-Site kaputt gehen könnte, sondern auch die Kette der Qualitätskontrolle. Für einzelne Entwickler und sehr kleine Teams wäre die Nutzung eines DevelopmentServers, auf dem lokale Änderungen vorgenommen werden, bevor sie an den ProductionServer committet werden, eine Minimalanforderung. Änderungen, die auf dem Development-Server gemacht und getestet wurden, werden erst dann auf den Production-Server hochgeladen, wenn alle Tests durchlaufen wurden. Wenn größere Teams die von mehreren Entwicklern vorgenommenen lokalen Änderungen koordinieren müssen, bevor man auf einen Live-Production-Server übergeht, braucht man dafür wahrscheinlich einen StagingServer. Es sollte hier noch betont werden, dass es von den Anforderungen des Teams und Projekts abhängt, was man als „Best Practices“ bezeichnen kann. In jeder dieser Prozessphasen kommen administrative Anforderungen und Kosten hinzu, die die Ressourcen von kleinen Teams belasten und die Komplexität von Low-Budget-Projekten erhöhen. Eine ideale Entwicklungsumgebung hat wenig Sinn, wenn sie zu einem administrativen Rückstand führt, der Verzögerungen im Projektablauf nach sich zieht. Wir werden ein Beispiel durchgehen, das darauf basiert, wie wir mit bestimmten Code dieses Buches gearbeitet haben, und bitten Sie dringend, sich daraus das herauszunehmen, was Sie für Ihre jeweiligen Anforderungen brauchen.
11.1.1 Designen für verschiedene Umgebungen Eines der Entwicklungsziele für jede Art von Applikation ist, dass sie in unterschiedlichen Umgebungen funktioniert und mit minimaler Neukonfiguration arbeiten kann. Das Zend Framework selbst erfordert nur PHP 5.1.4 oder höher und ansonsten nur ganz wenige spezielle Anforderungen. Von daher ist die Portabilität der Applikation weitgehend eine Frage des sorgfältigen Designs.
248
11.1 Den Server einrichten Die meisten der Unterschiede zwischen den jeweiligen Implementierungen einer Applikation kommen während der Initialisierung zum Tragen, wo die Konfigurationseinstellungen eingelesen werden. Bei einer Zend Framework-Applikation geschieht diese Differenzierung hauptsächlich in der Bootstrap-Datei. Bei der Places-Applikation haben wir das Bootstrapping in eine eigene Klasse ausgelagert, in die wie die Deployment-Umgebung als Parameter aus der Datei index.php im Root-Verzeichnis der Website übergeben können (siehe Listing 11.1). Listing 11.1 Der Inhalt der Datei index.php
Setzt zutreffende Pfade für Applikationsumgebung
set_include_path(get_include_path() . PATH_SEPARATOR . '/path/to/zf-working-copy/trunk/incubator/library/' . '/path/to/zf-working-copy/trunk/library/' ); include '../application/bootstrap.php'; Bindet Bootstrap-Datei $bootstrap = new Bootstrap('nick-dev'); ein und instanziiert mit $bootstrap->run();
Startet die Applikation
Konfigurationsabschnitt
Ihnen wird aufgefallen sein, dass ein zusätzlicher Vorteil bei diesem Vorgehen ist, dass wir damit alle für die Umgebung spezifischen Pfade setzen können – in diesem Fall zu einer lokalen Arbeitskopie (working copy) der aktuellsten Basiskomponenten des Zend Frameworks sowie die „Inkubator“-Komponenten. In Listing 11.1 haben wir diese Angabe über den nick-dev-Konfigurationsabschnitt gemacht, indem wir ihn vor Aufruf von run als Parameter an den Bootstrap-Konstruktor übergeben haben. Listing 11.2 zeigt eine gekürzte Version von bootstrap.php, in der hervorgehoben wird, wie die Deployment-Umgebung gesetzt und verwendet wird, wenn die Konfigurationseinstellungen aufgerufen werden. Listing 11.2 Gekürzte Version der Bootstrap-Klasse in bootstrap.php _deploymentEnv = $deploymentEnv; } public function run() { $config = new Zend_Config_Ini( 'config/config.ini', $this->_deploymentEnv);
Erhält Umgebungsparameter aus index.php
Initialisiert und startet Applikation
Setzt Zend_Config_Ini, damit der nick-dev-Abschnitt verwendet wird
Der zweite an Zend_Config_Ini übergebene Parameter in Listing 11.2 legt den Abschnitt fest, der aus config.ini ausgelesen werden soll. Der wird hier in der index.php vorgegeben,
249
11 Deployment nämlich nick-dev. Zend_Config_Ini unterstützt nicht nur INI-Abschnitte, sondern implementiert auch eine Vererbung von einem Abschnitt zum nächsten. Ein ererbter Abschnitt kann auch Werte überschreiben, die er von seinen Eltern geerbt hat. In Listing 11.3 sehen wir, dass der nick-dev-Abschnitt in der config-Datei die allgemeinen Einstellungen erbt, wie das durch die [child : parent]-Syntax der INI-Datei angegeben wird. Von daher werden die Datenbankeinstellungen auf jene einer lokalen Entwicklungsdatenbank zurückgesetzt. Listing 11.3 Der Inhalt der Datei config.ini [general] database.type = PDO_MYSQL database.host = localhost
Spezifiziert allgemeinen INI-Abschnitt, den alle anderen Abschnitte erben
Bisher haben wir die Implementierungsänderungen in die Konfigurationsdatei und in index.php aufgenommen. Besser noch, wir haben es sogar auf eine Weise gemacht, durch die wir diese Dateien auch verschieben können, ohne sie selbst verändern zu müssen. Um das noch weiter zu demonstrieren, schauen wir uns die index.php-Datei von Rob an, die nie von seinem lokalen Rechner verschoben werden muss: $bootstrap = new Bootstrap('rob-dev'); $bootstrap->run();
Auf dem Production-Server ist schließlich die unberührte Datei index.php mit ihren eigenen Einstellungen: $bootstrap = new Bootstrap('production'); $bootstrap->run();
Nachdem all das fertig eingerichtet ist, ist die einzige Datei, die zwischen den Umgebungen verschoben werden muss, die config.ini. Das kann immer die gleiche Datei sein, die in allen Phasen verwendet wird, weil darin die verschiedenen Abschnitte enthalten sind. Wenn die Applikation auf diese Weise zusammengehalten wird, vereinfacht das ein Verschieben der Dateien zwischen den Hosts, egal ob die Synchronisierung mit einem FTPClient Ihrer Wahl oder über ein geskriptetes Deployment stattfindet. Da wir gerade beim Thema Hosts sind, schauen wir uns an, wie man Hosts für unsere Development-Rechner einrichten kann.
250
11.1 Den Server einrichten
11.1.2 Die Arbeit mit virtuellen Hosts in der Entwicklung Nach Skizzieren des Einsatzes von getrennten Hosting-Umgebungen für die unterschiedlichen Phasen der Entwicklung sollten wir nun ein Beispiel durchgehen, wie man das Hosting selbst passend einrichten kann. In diesem Abschnitt beschäftigen wir uns mit einem kurzen Beispiel, das mit einem Apache Webserver auf einem Unix-Rechner in einem kleinen lokalen Netzwerk arbeitet. Das virtuelle Hosting ist eine Methode, wie man mehrere Websites von einem einzigen Rechner aus versorgen kann. Der Einsatz eines einzelnen Rechners reduziert nicht nur die Kosten der Hardware, sondern auch die für Wartung und Pflege dieser Maschine erforderliche Zeit. Die beiden Hauptvarianten virtueller Hosts sind namensbasierte virtuelle Hosts, die sich die gleiche IP-Adresse teilen, und IP-basierte virtuelle Hosts, bei dem jeder seine eigene IP-Adresse bekommt. Aus Gründen der Einfachheit entscheiden wir uns hier fürs namensbasierte Hosting. Nach der Einrichtung sollten wir auf die verschiedenen Entwicklungsphasen mit URLs wie http://places-dev/ und http://places-stage/ zugreifen können. Tipp
Zend Framework leitet mittels mod_rewrite alle Requests über die Front-Controller-Datei. Kombinieren Sie das mit seiner Routing-Funktionalität, und Sie erhöhen die Fehlerquellen, die aus Problemen mit dem Pfad herrühren. Die Einrichtung von virtuellen Hosts ist eine Möglichkeit, damit man keine Zeit mehr mit Pfadproblemen verschwenden muss, weil http://127.0.0.1/~places-dev zu http://places-dev/ werden kann.
11.1.2.1 Die Hosts-Datei einrichten Die Hosts-Datei speichert die Informationen, die den Hostnamen einer IP-Adresse zuordnet (mappt). Beim namensbasierten Hosting wird eine Adresse für mehrere Namen verwendet. Wenn wir uns also die Hosts-Datei in /etc/hosts anschauen, sehen wir den folgenden Eintrag: 127.0.0.1 places-dev 127.0.0.1 places-stage
Die Namen places-dev und places-stage werden beide auf die localhost-IP-Adresse 127.0.0.1 gemappt, was bedeutet, dass alle Anfragen, die dieser Rechner für diese Namen bekommt, auf seinen eigenen Webserver geleitet werden. Anstatt zu versuchen, ein komplettes DNS zu konfigurieren, werden wir auch die Hosts-Datei einer der vernetzten Rechner konfigurieren, damit die gleichen Namen auf die IP-Adresse des Host-Rechners gelenkt werden: 192.168.1.110 places-dev 192.168.1.110 places-stage
Diese Einstellungen in jedem Netzwerkrechner gewährleisten, dass Anfragen an placesund places-stage an die IP des Rechners geleitet werden, der uns als Webserver dient. Denken Sie daran, dass die IP-Adresse des Rechners, mit dem Sie arbeiten, vielleicht nicht die gleiche ist wie die in diesem Beispiel. Das gilt auch für die vielen Einstel-
dev
251
11 Deployment lungen in diesem Kapitel, einschließlich der folgenden Apache-Konfigurationseinstellungen. 11.1.2.2 Apache konfigurieren Nach dem Einrichten der Hosts-Datei müssen wir nun die virtuellen Hosts in der ApacheKonfigurationsdatei httpd.conf konfigurieren (siehe Listing 11.4). Listing 11.4 Die Einstellungen für die virtuellen Hosts in der Apache-Datei httpd.conf NameVirtualHost *:80 Setzt ApacheOptions Indexes MultiViews Standardverzeichnisse AllowOverride All Erlaubt Verwendung Order allow,deny von .htaccess Allow from all Setzt Einstellungen
Setzt IP-Adresse und Port, der auf Requests lauschen soll
für Development-Host DocumentRoot /path/to/Sites/places-dev/web_root ServerName places-dev
In Listing 11.4 spezifiziert die NameVirtualHost-Direktive, dass alles, was auf Port 80 eingeht, Anfragen für die namensbasierten virtuellen Hosts empfangen wird. Der Verzeichnisabschnitt definiert einige allgemeine Einstellungen für alle virtuellen Hostverzeichnisse. Besonders anzumerken ist die AllowOverride-Einstellung, die erlaubt, das Verzeichnisse bestimmte Servereinstellungen anhand von .htaccess-Dateien überschreiben kann. Das ist eine Anforderung bei auf Zend Framework basierenden Sites, weil mod_rewrite verwendet wird, um alle eingehenden Anfragen durch die index.php-Datei zu leiten. Jeder VirtualHost-Abschnitt definiert den vollständigen Pfad zum Root-Verzeichnis und den Namen des Servers. Nach Angabe von DocumentRoot in httpd.conf müssen wir diese Verzeichnisse erstellen: $ cd Sites/ $ mkdir -p places-dev/web_root $ mkdir -p places-stage/web_root
Wenn Apache nun neu gestartet wird, greift er diese neuen Einstellungen auf: $ sudo apachectl graceful
Wenn alles geklappt hat, sollten wir bei Eingabe von http://places-dev in den Browser auf einem der beiden Rechner (deren Host-Dateien wir bearbeitet haben) zu den Dateien kommen, die sich in /Pfad/zu/Sites/places-dev/web_root auf der Hosting-Maschine befinden.
252
11.2 Versionskontrolle mit Subversion Natürlich befinden sich momentan noch keine Dateien im Hosting-Space. Um diese Dateien zu bekommen, müssen wir sie aus dem Repository zur Versionskontrolle auschecken.
11.2 Versionskontrolle mit Subversion Weil Rob sich in England befindet, Nick und Steven aber in Australien leben, war die Gemeinschaftsarbeit am Code für dieses Buch nicht so einfach wie mal eben über den Tisch zu rufen. Damit wir bei unseren Bearbeitungen auf dem Laufenden bleiben, brauchen wir irgendeine Art von Versionskontrolle. Es gibt verschiedene Systeme dafür, die unterschiedliche Funktionalität anbieten. Wir haben uns für eines entschieden, das auch in der Entwicklung von Zend Framework eingesetzt wird, nämlich Subversion. Subversion ist ein Open Source-System, das Änderungen an Dateien sowie Verzeichnissen sortieren, speichern und aufzeichnen kann, während außerdem die gemeinschaftliche Nutzung dieser Daten in einem Netzwerk gemanagt wird. Wenn Sie sich mit dem Prozess der Versionskontrolle (und mit Subversion im Speziellen) vertraut machen, können Sie enger am Repository des Frameworks arbeiten. Gleichzeitig eigenen Sie sich einen Workflow an, der Ihre Deployment-Praktiken weiter verbessern wird. Wir werden einige der alltäglichen Einsatzmöglichkeiten der Versionskontrolle mit Subversion durchgehen. Unsere Absicht ist, Ihnen einen Überblick über den Ablauf zu geben und Ihnen das Vertrauen zu vermitteln, selbst weiter zu forschen. Um einige der Beispiele selbst ausprobieren zu können, müssen Sie Zugang zu einem Subversion-Host haben. Das ist zugegebenermaßen ein bisschen wie mit der Henne und dem Ei: Das Einrichten eines Subversion-Servers sprengt den Rahmen dieses Kapitels. Wir gehen von daher davon aus, dass Sie einen solchen Server verfügbar haben, und konzentrieren uns darauf, wie man ein Projekt in Subversion einrichtet und damit arbeitet. Sie finden Informationen über Subversion online oder in Büchern wie dem E-Book Subversion in Action von Jeffrey Machols (www.manning.com/machols).
11.2.1 Erstellen des Subversion-Repositorys Um Projekte in Subversion zu speichern, müssen wir zuerst ein Repository erstellen. Weil man kein Repository auf einem Netzwerklaufwerk erstellen kann, muss das lokal über ein Laufwerk auf dem gleichen Rechner geschehen. Um das Repository für Places zu erstellen, geben wir den folgenden Befehl ein: svnadmin create /Pfad/zum/Repository/places/
Das Subversion-Projekt empfiehlt offiziell, die drei Verzeichnisse trunk/, tags/ und branches/ unter einer beliebigen Zahl von Projekt-Roots einzurichten. Trunk ist ganz offensichtlich das Hauptverzeichnis, wo der Großteil der Aktionen stattfindet. Das tagsVerzeichnis enthält aussagekräftig getaggte (etikettierte) Kopien. Wir könnten uns z. B.
253
11 Deployment entscheiden, eine Phase in der Entwicklung von Places im Kontext des Schreibens dieses Buches zu markieren: $ svn copy http://svn.example.com/svn/places/trunk \ http://svn.example.com/svn/places/tags/chapter-04 \ -m "Tagging places as snapshot of development at chapter 4."
Das Verzeichnis branches/ enthält verzweigte (branch, engl. Zweig) Kopien des Dateisystems, so wie jene, die wir erstellen würden, wenn wir mit einer wesentlichen Änderung in der Architektur von Places experimentieren wollen. Das Verzweigen wird etwas später in diesem Kapitel noch Thema sein. Weil wir diese Verzeichnisse lokal einrichten und unter Versionskontrolle stellen wollen, machen wir das über den Befehl svn mkdir: $ $ $ $
Nach Erstellung der relevanten Verzeichnisse können wir das teilweise fertige Projekt importieren, mit dem wir angefangen haben: $ svn import /Pfad/zu/places-Datei:////Pfad/zu/svn/places/trunk/ \ -m "Initial import" Adding places/web_root/index.php Adding places/web_root/.htaccess Adding places/application Committed revision 1
Nach Erstellung dieser Verzeichnisse und dem Import der bisherigen Projektmaterialien in das Subversion-Repository können wir damit beginnen, es bei der Entwicklung einzusetzen. Abbildung 11.1 zeigt die Verzeichnisstruktur von Subversion nach dem ersten Commit.
branches
.htaccess web_root
places
index.php
trunk application tags
Abbildung 11.1 Das Subversion-Repository von Places nach dem ersten Commit
Da die Grundstruktur nun bereit ist, müssen wir jetzt eine Arbeitskopie auf unseren lokalen Rechnern erstellen – das heißt also, wir checken es aus dem Repository aus.
11.2.2 Code aus dem Repository auschecken Nachdem wir das Projekt-Repository in dieser Phase eingerichtet haben, machen unsere Leute auf dieser Seite der Welt erst einmal Feierabend und lassen die auf der anderen Seite
254
11.2 Versionskontrolle mit Subversion weiterarbeiten. Wenn’s dann bei uns wieder dämmert, platzt das Repository wegen der neuen Dateien förmlich aus allen Nähten. Die können wir dann als Arbeitskopie in unseren lokalen Entwicklungsrechner auschecken (in gekürzter Form steht das in Listing 11.5). Listing 11.5 Die neuesten Arbeiten aus dem Repository auschecken $ svn checkout \ http://www.example.com/svn/places/trunk/ places/ A places/web_root A places/web_root/css ... A places/db A places/db/test_data.sql Checked out revision 2.
Lokales PlacesVerzeichnis
Nach dem Auschecken einer Arbeitskopie der Dateien und etwaigen Änderungen ist der nächste Schritt, diese Änderungen wieder in das Repository zu committen.
11.2.3 Änderungen ins Repository committen Mit einem Commit schicken Sie die Änderungen an Ihrer Arbeitskopie ins SubversionRepository, meist begleitet von einer kurzen, erläuternden Nachricht. An diesem Punkt ist es sehr wertvoll, sich gute Gewohnheiten fürs Committen zuzulegen. Die wichtigste davon ist, den Zweck Ihrer Änderungen so treffend wie möglich zu halten. Stellen Sie sich nur einmal vor, dass Kollegen Ihres Teams einen großen Haufen Änderungen herausklamüsern müssen, die sich über eine Vielzahl von Dateien verstreuen können, und Sie werden erkennen, wie viel Extraarbeit Sie ihnen aufgedrückt haben, vor allem, wenn Ihre Änderungen eventuell in Konflikt mit denen der Kollegen geraten. Sobald Ihre Commit-Infos unklar sowie schwer zu beschreiben und festzuhalten werden, können Sie davon ausgehen, dass die Änderungen mangelhaft getroffen wurden. Die meisten Teams haben zumindest ein paar grundlegende Richtlinien für das Committing, z. B. Verweisnummern für den Issue-Tracker in der Commit-Nachricht. Wenn wir unsere neue Kopie von Places durchgehen, fällt als Erstes auf, dass die Datenbankeinstellungen in config.ini sich von denen unterscheiden, die wir für unseren lokalen Datenbankserver brauchen. Das müssten wir also schon mal ändern. Für den Moment können wir eine Kopie machen, die config.ini.default heißen soll, und das Original nehmen, damit die lokale Applikation auch funktioniert. Letzten Endes werden wir die configDatei konsistenter einrichten, aber jetzt committen wir einfach config.ini.default (siehe Listing 11.6), damit spätere Commits nicht unsere eigenen config-Dateien überschreiben.
255
11 Deployment Listing 11.6 Änderungen ins Repository committen $ cd ~/Sites/places $ svn status | Holt Feedback über den ? application/configuration/config.ini.default Status der Arbeitskopie $ svn add application/configuration/config.ini.default A application/configuration/config.ini.default $ svn status Committet Arbeitskopie A application/configuration/config.ini.default ins Repository $ svn commit -m "Added .default config file." Adding application/configuration/config.ini.default Transmitting file data ...... Committed revision 3.
In Listing 11.6 werden Ihnen vielleicht die wiederholten Prüfungen des Status aufgefallen sein. Damit wir sicher sein können, dass die Arbeitskopie so wie erwartet ist, sollte man sich immer angewöhnen, den Status zu prüfen, bevor weitere Aktionen vorgenommen werden. Weil alle Nutzer des Repositorys ebenfalls Änderungen committen, werden wir darüber auf dem Laufenden bleiben müssen, indem wir den Befehl svn update verwenden.
11.2.4 Aktualisierung einer lokalen Arbeitskopie In jeder Phase der Entwicklung müssen wir den Status unserer Arbeitskopie überprüfen, damit wir wissen, ob sie zum Repository aktuell ist. Nicht überraschend, dass der Befehl svn update sich um die Aktualisierung einer lokalen Arbeitskopie mit den neuesten Bearbeitungen kümmert und dass diese Änderungen vom Output des Befehls angezeigt werden. In Listing 11.7 führen wir eine Aktualisierung der Arbeitskopie durch. Listing 11.7 Aktualisierung der Arbeitskopie aus dem Repository
Prüft vorm Fortfahren den $ cd ~/Sites/places Status der Arbeitskopie $ svn status $ svn update Initiiert die A application/classes Update-Action A application/classes/Places A application/classes/Places/Controller A application/classes/Places/Controller/Action A application/classes/Places/Controller/Action/Helper A application/classes/Places/Controller/Action/Helper/ViewRenderer.php Updated to revision 4.
Wie durch den Buchstaben A vor jeder Output-Zeile in Listing 11.7 angezeigt wird, wurde die Datei ViewRenderer.php zusammen mit den übergeordneten Verzeichnissen hinzugefügt. Das war ein einfaches Update, und bei der Arbeitskopie gibt es keine Konflikte. Manchmal kommen die allerdings doch vor, und wir müssen wissen, wie damit umzugehen ist.
256
11.2 Versionskontrolle mit Subversion
11.2.5 Der Umgang mit Konflikten Subversion kann Änderungen managen, sogar wenn zwei Personen gleichzeitig die gleiche Datei bearbeitet haben, indem einfach bei Bedarf die Änderungen kombiniert werden. Und doch können Konflikte entstehen, wenn die Änderungen in der gleichen Zeile einer Datei erfolgt sind. Das Folgende kommt beispielsweise vor, wenn eine spätere Aktualisierung zu einem Konflikt mit bootstrap.php führt, was durch den vorangestellten Buchstaben C angezeigt wird: $ svn update C application/bootstrap.php Updated to revision 5.
In Abbildung 11.2 sehen wir, dass das Update vier Variationen der vom Konflikt betroffenen Datei produziert hat.
bootstrap.php.r1110 ist das Original vor der lokalen Modifikation. Das „r1110“ ist die Revisionsnummer der Datei beim vorigen Update.
bootstrap.php.r1112 ist die aktuellste Revision, die jemand in das Repository committet hat.
bootstrap.php.mine ist unsere modifizierte lokale Arbeitskopie. bootstrap.php enthält die Unterschiede zwischen den Dateien der beiden Versionen, auch als diff (für difference) bezeichnet.
bootstrap.php
bootstrap.php.mine application bootstrap.php.r180
bootstrap.php.r182
Abbildung 11.2 Die vier in Konflikt stehenden Varianten der Datei bootstrap.php nach
svn update
Mit einem Blick in bootstrap.php sehen wir die folgenden Zeilen, die den Konflikt auslösen: <<<<<<< .mine Zend_Controller_Action_HelperBroker::addPrefix( 'Places_Controller_Action_Helper' ); ======= Zend_Controller_Action_HelperBroker::addPrefix( 'Zend_Controller_Action_Helper_' ); >>>>>>> .r1112
Das ist in etwa ähnlich wie der Output, der durch Ausführen dieses Befehls generiert wird: diff bootstrap.php.mine bootstrap.php.r1112
257
11 Deployment Von hier sehen wir, dass unser Bearbeiter .mine den Unterstrich am Ende des Parameterstrings Places_Controller_Action_Helper entfernt hat und die andere Bearbeitung den Start des Parameternamens von Places_ zu Zend_ geändert hat. Nach einer kurzen Absprache beschließen wir, dass keine unserer Änderungen benötigt wird und wir auf die Version vor unseren Bearbeitungen zurückgreifen sollten. Hier handelt es sich um einen simplen Fix: Wir könnten entweder bootstrap.php bearbeiten oder bootstrap.php.r1110 auf bootstrap.php kopieren. Für dieses Beispiel wollen wir nun bootstrap.php bearbeiten, sodass sie nur diese Zeile enthält: Zend_Controller_Action_HelperBroker::addPrefix( 'Places_Controller_Action_Helper_' );
Anschließend ergibt ein kurzer Status-Check, dass wir zwar die Datei korrigiert haben, aber der Subversion-Konflikt immer noch existiert: $ svn status C application/bootstrap.php
Weil wir beschlossen haben, dass von den vier Variationen nun die Datei bootstrap.php jene ist, mit der wir weiterarbeiten wollen, müssen wir Subversion mitteilen, dass das Problem behoben worden ist, und mit welcher Datei wir weitermachen wollen: $ svn resolved application/bootstrap.php Resolved conflicted state of 'application/bootstrap.php'
Nachdem das erledigt ist, ergibt eine weitere Prüfung durch svn status, dass die Datei nun als modifiziert gekennzeichnet wurde: $ svn status M application/bootstrap.php
Die anderen drei Dateien sind infolge des Auflösungsprozesses auch entfernt worden, sodass bloß noch bootstrap.php übrig bleibt: $ ls bootstrap.php config.ini controllers views classes configuration models
Der abschließende Schritt ist, die Änderungen ins Repository zu committen und eine Info einzufügen, die die Auflösung vermerkt: $ svn commit -m "Resolved change conflict with bootstrap.php" Sending application/bootstrap.php Transmitting file data. Committed revision 6.
Damit haben Sie nun einen kurzen Einblick in ein paar übliche alltägliche Aufgaben bekommen, die bei Subversion anfallen. Nun bleiben nur noch ein paar erwähnenswerte Themen übrig, und wir fangen damit an, wie wir an eine saubere Kopie des Codes aus dem Repository kommen.
258
11.2 Versionskontrolle mit Subversion
11.2.6 Eine saubere Kopie aus dem Repository holen Subversion speichert seine Informationen in eigenen .svn-Verzeichnissen in jedem Verzeichnis des Repositorys. Diese .svn-Verzeichnisse sind notwendig, während der Inhalt sich unter Versionskontrolle befindet, doch sie sollen sicher nicht mit ins finale Release, wenn Sie beispielsweise die Inhalte per FTP auf den Server laden. Listing 11.8 zeigt, wie Sie mit dem export-Befehl eine Kopie aus dem Repository ohne all diese versteckten Verzeichnisse bekommen (der Output ist gekürzt worden). Listing 11.8 Mit dem Befehl export eine saubere Arbeitskopie holen $ svn export \ http://www.example.com/svn/places/trunk/ places_export/ A places_export A places_export/web_root ... A places_export/db A places_export/db/test_data.sql Exported revision 7.
Gibt Quell- und Zielverzeichnisse an
Beachten Sie, dass wir nach dem Export aus Listing 11.8 die gleichen Dateien bekommen wie nach einem Checkout, aber weil keine .svn-Verzeichnisse dabei sind, stehen sie nicht unter Versionskontrolle. Ein Grund, warum wir eine saubere Kopie des Codes exportieren wollen, ist, damit wir damit in einem anderen Projekt arbeiten können oder mit dem Code in eine andere Richtung weiterarbeiten wollen. Es gibt aber noch eine andere Möglichkeit, den Code in eine andere Richtung voranzutreiben, indem wir nämlich mit Branches (wörtlich: Verzweigungen) arbeiten.
11.2.7 Die Arbeit mit Branches Wir haben ein Beispiel gegeben, wie man mit Tagging spezielle Punkte im Ablauf dieses Buches markieren kann, doch was ist mit größeren Gelegenheiten, wenn wir es beispielsweise beenden? Dazu könnten wir die Verzweigungsfähigkeiten von Subversion nutzen: $ svn copy http://www.example.com/svn/places/trunk/ \ http://svn.example.com/places/branches/its-finished \ -m "Woohoo, we've finished the book!" Committed revision 200.
Wie der Name schon sagt und die Abbildung 11.3 veranschaulicht, wird durch eine Verzweigung ein vom Haupt-Trunk unabhängiger Zeitstrang erstellt. Es gibt verschiedene Gründe, warum man den Code verzweigen lassen kann. Zend Framework wird bei jedem offiziellen kleineren oder größeren Release verzweigt, doch wir könnten uns auch dafür entscheiden, dass eine neue Verzweigung für eine eigene Version des Haupt-Codes oder für eine experimentelle Version passieren soll.
259
11 Deployment places/branches/experiment/
places/trunk/
places/branches/its-finished
Abbildung 11.3 Verzweigungen in einem Subversion-Repository
Das Thema der Verzweigungen könnte ein ganzes Kapitel füllen, also soll durch diese kurze Erwähnung nur dessen Einsatz angesprochen werden. Wenn Sie mehr darüber wissen wollen, schauen Sie sich das E-Book Subversion in Action von Jeffrey Machols an (www.manning.com/machols). Das letzte erwähnenswerte Thema ist, wie man externen Code einbindet, auf dem Ihr eigener Code aufbaut.
11.2.8 Externer Code Als wir Dateien zwischen den Arbeitskopien und dem Repository verschoben haben, gab es einen unberücksichtigten Aspekt: Die Places-Applikation braucht Code des Zend Frameworks, der für den Places-Code extern ist. Wie können wir sicherstellen, dass die Revisionen in unserem Code zu externen Code-Abhängigkeiten passen? Wir können vermeiden, uns damit herumschlagen zu müssen, indem wir eines unserer lokalen Verzeichnisse auf die externe URL des Repositorys des Zend Frameworks mappen: svn propedit svn:externals . application/library/Zend \ http://framework.zend.com/svn/framework/standard/branches/release1.5/library/Zend/
Nun wird jeder Checkout des Repositorys, in dem dieses lokale Verzeichnis enthalten ist, auch den Zend Framework-Code auschecken. Beachten Sie, dass wir, um Probleme mit nicht zusammenpassenden Code-Versionen zu vermeiden, auf einen stabilen ReleaseZweig gezeigt haben anstatt auf einen Zweig, der zu häufig geändert wird, z. B. trunk. Eine feststehende Regel der Versionskontrolle ist, niemals kaputten Code zu committen, und das kann man u. a. damit verhindern, dass der Code vor dem Committen gründlich getestet wird. Im nächsten Abschnitt schauen wir uns eine Möglichkeit an, wie man die Systeme testet, die wir entwickeln.
11.3 Funktionale Tests Im ganzen Buch haben wir stets darauf verwiesen, dass für den Code, an dem wir arbeiten, Code-Tests vorgenommen werden müssen. Die Kombination aus gründlicher CodeAbdeckung der Unit-Tests des Zend Frameworks mit den applikationsspezifischen UnitTests sollte aus Sicht des Programmierers für ein fortlaufendes Feedback sorgen, dass der Code sich wie erwartet verhält. Doch sogar ein System, das erfolgreich alle Unit-Tests
260
11.3 Funktionale Tests besteht, könnte in anderen Bereichen die Erwartungen nicht erfüllen, z. B. bei der Frage, wie das System im Einsatz tatsächlich funktioniert. Um dies zu testen, wenden wir uns den sogenannten funktionalen Tests zu. Während man die Funktionalität des Systems auf spezifische Testbereiche wie Benutzerfreundlichkeit (Usability), Sicherheit und Performance reduzieren kann, werden wir hier allgemeine Testmethoden umreißen, wie das System aus der Perspektive der Endnutzer zu prüfen ist. Wir werden testen, was der User mit dem System machen kann. Bei Sicherheitstests können wir beispielsweise untersuchen, ob ein nicht eingeloggter User auf einen gesicherten Bereich zugreifen kann. Weil wir mit Zend Framework meistens Webapplikationen entwickeln, werden Tests aus der Perspektive des Endusers am besten über einen Browser nachgebildet. Dafür eignet sich das Testtool Selenium IDE ideal.
11.3.1 Funktionales Testen mit Selenium IDE Selenium IDE ist ein Tool, mit dem Webapplikationen auf die gleiche Weise getestet werden, wie man sie verwendet: mit einem Browser. Damit kann man Aktionen aufzeichnen, bearbeiten und abspielen. Wenn die Aktionen aufgezeichnet sind, können Sie sie erneut starten und mit oder ohne Start- und Breakpoints auf einen Rutsch oder schrittweise durchgehen. Selenium IDE ist vor allem deswegen so praktisch, weil das Unit-Test-Tool PHPUnit, das vom Zend Framework verwendet wird, auch eine Selenium-RC-Erweiterung besitzt. Das bedeutet, wir können die Selenium IDE-Tests in unsere gesamte Testprozedur integrieren. Hoffentlich haben Sie PHPUnit schon installiert, denn wir schauen uns nun die Installation von Selenium IDE an. Anmerkung
Die Installation von PHPUnit wird in Kapitel 3 erläutert.
11.3.1.1 Die Installation des Selenium IDE Weil die Selenium IDE ein Add-on für den Firefox-Browser ist, müssen Sie Firefox installiert haben, bevor Sie die Extension installieren können (verfügbar unter http://www. mozilla.com/firefox/). Die Selenium IDE kann auf der Firefox-Seite mit den Add-ons heruntergeladen werden (https://addons.mozilla.org/firefox) oder von der eigenen Download-Seite des Projekts (http://www.openqa.org/selenium-ide/download.action). Die einfachste Methode der Installation ist, in Firefox auf den Link zur .xpi-Datei zu klicken, damit Firefox sich um Download und Installation kümmert.
261
11 Deployment 11.3.1.2 Aufzeichnung eines Selenium IDE-Tests Um einen Test aufzuzeichnen, wählen Sie Selenium IDE aus dem Tools-Menü in Firefox aus, das Firefox automatisch in den Aufzeichnungsmodus versetzt. Als Nächstes besuchen Sie die Webseiten wie gewohnt. Wir wollen nun einen ganz einfachen Test festhalten, bei dem nach „zend framework“ gegoogelt und dann den Links auf die Seite mit dem Manual fürs Zend Framework gefolgt wird. Dazu gehören die folgenden Schritte: 1. Gehen Sie zu google.com.au 2. Geben Sie im Suchfeld „zend framework“ ein und klicken Sie auf „Google-Suche“. 3. Nachdem die Liste der Treffer erschienen ist, klicken Sie auf den Link von http://framework.zend.com. 4. Auf der Homepage des Zend Frameworks klicken Sie auf den Link zur Dokumentation, und dann wählen Sie Reference Guide von der Drop-down-Liste, um ans Manual zu kommen. Wenn Sie damit fertig sind, beenden Sie die Aufzeichnung durch Klick auf die rote Schaltfläche oben rechts im Fenster. Wenn alles geklappt hat, stellt Selenium IDE die aufgezeichneten Aktionen unter dem Table-Tab bereit (siehe Abbildung 11.4).
Abbildung 11.4 Selenium IDE mit Table-Tab und den Ergebnissen der Aufzeichnung
Wenn nicht alles geklappt hat, können Sie den Vorgang entweder wiederholen und die Schritte nach Bedarf verändern oder Sie bearbeiten die Schritte, die Sie bereits aufgezeichnet haben. 11.3.1.3 Die Bearbeitung des Tests Wenn Sie aufgepasst haben, wird Ihnen sicher in Abbildung 11.4 aufgefallen sein, dass wir aus Versehen auf den Download-Link der Homepage von Zend Framework geklickt haben. Weil das nicht zum Test gehören soll, werden wir diesen Schritt bearbeiten. Wir können das machen, indem wir auf den Source-Tab des Selenium IDE-Fensters wechseln, der das HTML aus Abbildung 11.5 zeigt.
262
11.3 Funktionale Tests Die Zeilen, die wir aus dem Quellcode in Abbildung 11.5 entfernen müssten, sind die folgenden:
clickAndWait
link=Download
Das kann direkt im Source-Tab bearbeitet werden. Nach Entfernen dieser Zeilen können wir alles mit Klick auf den Play-Button noch einmal abspielen. 11.3.1.4 Speichern des Tests Wenn der Test wie erwartet durchgeführt wurde, können wir ihn für zukünftige Zwecke speichern. Wenn Selenium IDE aktiv ist, werden bei Wahl von „File“ aus der Menüleiste mehrere Optionen zum Speichern von Tests angeboten. Bei diesem Beispiel wählen wir Save Test As, womit der Test im Standard-HTML-Format gespeichert wird, das wir in Abbildung 11.5 gesehen haben. Er kann später wieder geöffnet und der Test bei Bedarf auch wiederholt werden, idealerweise automatisch.
Abbildung 11.5 Selenium IDE mit Source-Ansicht mit den Ergebnissen der Aufzeichnung
263
11 Deployment
11.3.2 Automatisierung der Selenium IDE-Tests Im vorigen Beispiel haben wir einen einzelnen Test aufgezeichnet und ihn komplett im Browser durchgeführt, und zwar im sogenannten Test Runner-Modus. Das ist für einzelne Tests ganz prima, aber damit wir auch wirklich produktiv sind, brauchen wir Selenium RC, mit dem wir mehrere Selenium IDE-Tests durchführen können, die in einer Programmiersprache geschrieben wurden. Eine der unterstützten Sprachen ist natürlich PHP, das als „PHP-Selenium RC“ zur Verfügung steht, wenn man den Test unter Export Test As abspeichert. Leider enthält, während wir dies schreiben, die resultierende Exportdatei Code, der nicht nur sehr weitschweifig ist, sondern leider auch nicht die neuere Extension PHPUnit_Extensions_SeleniumTestCase PHPUnit verwendet. Listing 11.9 zeigt den exportierten Selenium IDE-Test als PHPUnitTestfall. Bei dem entscheiden wir uns, ihn aus Gründen der besseren Effizienz umzuschreiben. Ob Sie Ihre eigenen Tests umschreiben müssen, wird von Ihren persönlichen Vorlieben abhängen und auch davon, ob es für die Selenium IDE Updates gibt. Listing 11.9 Der Selenium IDE-Test als PHPUnit-Testfall setBrowser('*firefox'); $this->setBrowserUrl('http://www.google.com.au/'); } function testMyTestCase() Repliziert den in { der Selenium IDE $this->open('http://www.google.com.au/'); durchgeführten Test $this->type('q', 'zend framework'); $this->click('btnG'); $this->waitForPageToLoad('30000'); try { $this->assertTrue($this->isTextPresent('framework.zend.com/')); } catch (PHPUnit_Framework_AssertionFailedError $e) { array_push($this->verificationErrors, $e->toString()); } } }
Beachten Sie, dass bei unserem Testfall mit der Selenium IDE der Teil fehlt, der in der zend.com-Domäne operiert. Weil Selenium RC teilweise außerhalb des Browsers läuft, sind wir auf Probleme mit der Richtlinie des gleichen Ursprungs (same origin issue) gestoßen, die verhindert, dass ein Dokument oder Skript, „das von einem bestimmten Ursprung her geladen wurde, für ein Dokument von anderer Herkunft Eigenschaften setzt oder ausliest“. Der Originaltest, den wir mit Selenium IDE aufgezeichnet haben, funktionierte, weil er als Extension komplett innerhalb des Browsers ausgeführt wurde. Bei Sele-
264
11.3 Funktionale Tests nium RC werden die Seiten durch einen Proxy geschickt, der teilweise das Problem mit dem gleichen Ursprung umgeht, aber die Unterstützung für den Wechsel von Domänen befindet sich aktuell noch im Teststadium. Nachdem wir unseren Test geschrieben haben, können wir ihn fast schon starten, doch weil er auf dem Selenium RC-Server aufbaut, müssen wir den von http://www.openqa.org/ selenium-rc/download.action herunterladen und ihn mit diesem Befehl starten: java -jar /Pfad/zum/Server/selenium-server.jar
Wenn der Server erst einmal läuft, können wir den Test wie in Abbildung 11.6 gezeigt starten.
Abbildung 11.6 Das Unit-Test-Beispiel für Selenium IDE wird anhand von PHPUnit über den Selenium RC-Server gestartet.
Prima – Test bestanden. Nachdem unser Selenium IDE-Test nun als PHPUnit-Unit-Test geschrieben wurde, können wir weitermachen, indem wir Tests einbauen und sie als Teil einer umfassenderen Testsuite aufnehmen, die über einen einzigen Befehl gestartet werden kann.
11.3.3 Funktionstests mit Zend_Http_Client Das Manual stellt fest: „Der Zend_Http_Client bietet ein einfaches Interface zur Durchführung von HTTP-Requests (Hyper-Text Transfer Protocol). Auf der einfachsten Stufe ist das etwas wie der Firefox-Browser. Damit schauen wir uns nun das Listing 11.10 an, das ein Beispiel für die Tests zeigt, die wir mit dem Selenium IDE durchgeführt haben, das so umgeschrieben wurde, um mit dem Zend_Http_Client zu arbeiten.
265
11 Deployment Listing 11.10 Der umgeschriebene Selenium IDE-Test verwendet nun den Zend_Http_Client. request(); Anfrage an Google $this->assertContains( HTML enthält '', $response->getBody() ); }
public function testQueryGoogle() { $client = new Zend_Http_Client( 'http://www.google.com.au/search?q=zend+framework' ); $response = $client->request(); $this->assertContains('framework.zend.com/', $response->getBody()); }
Achtet darauf, dass Suchanfrage an Google einen String enthält
}
Die Ähnlichkeiten zwischen diesem Code und dem vorigen Selenium IDE-Test sind recht offensichtlich. Also braucht man nichts Weiteres zu tun, als nur den Test zu starten. Die Ergebnisse werden in Abbildung 11.7 gezeigt.
Abbildung 11.7 Ein Fehler aufgrund eines Tippfehlers beim ersten Start des Unit-Tests
266
11.4 Das Skripting des Deployments Wegen eines Tippfehlers ( anstatt ) gibt es eine Fehlermeldung. Nach der Korrektur prüfen wir das Ergebnis erneut durch Start des Tests (siehe Abbildung 11.8).
Abbildung 11.8 Der finale Unit-Test ist erfolgreich!
Ausgezeichnet – das hat nun geklappt, und so wie die auf dem Selenium IDE basierenden Tests können sie prima in die Testsuite eingebaut werden. Zend_Http_Client verfügt aktuell nicht über alle Fähigkeiten wie Selenium IDE, z. B. die Möglichkeit, JavaScript zu testen, doch es ist ein relativ simpler Weg zur Erstellung von Funktionstests. Wir sind mit diesem Kapitel fast zu Ende, doch werden noch kurz einen Aspekt des Deployments ansprechen, der die innere Faulheit aller Entwickler reizen wird: das Skripting des Deployment-Vorgangs.
11.4 Das Skripting des Deployments Wir haben kurz den Einsatz des Befehls svn export erwähnt, um eine saubere Version eines Subversion-Repositorys zu bekommen, das wir per FTP auf einen Production-Server transferieren können. Diese Lösung wäre praktisch, wenn wir beispielsweise auf dem Production-Server eines Kunden arbeiten, den wir nicht kontrollieren können. Wäre das aber doch der Fall und wir könnten den Server steuern, dann gibt es keinen Grund, warum dieser Production-Server nicht auch unter Versionskontrolle stehen und über den Befehl svn update aktualisiert werden könnte. Weil wir bereits viel Zeit mit der Erläuterung der Automatisierung von Tests verbracht haben, wäre die naheliegende Erweiterung, das Testen und Deployment des Codes zwischen den verschiedenen Umgebungen zu automatisieren. Leider ist das Skripting von Deployments ein zu umfassendes Thema, als dass wir es in wenigen Absätzen in diesem Kapitel abhandeln könnten, und es ist auch viel zu sehr auf die jeweiligen Arbeitsanforderungen der Teams bezogen. Wir können Ihnen als Inspiration allerdings das build-tools-Verzeichnis des Zend Frameworks nennen. Dies steht unter http://framework.zend.com/svn/framework/build-tools/ bereit und enthält eine Reihe von Shell-Skripten, die aus dem Repository exportieren, die Dokumentation generieren und die zip- und tarball-Archive, die man von der Download-Seite des Zend Frameworks herunterladen kann, erstellen können.
267
11 Deployment Eine weitere Option wäre, alle automatisierten Tests durchzuführen und mit dem Deployment erst dann fortzufahren, wenn die Tests komplett ohne Probleme abgeschlossen wurden. Die Ausführung dieser Deployment-Skripte kann auch automatisiert werden, vielleicht auf einer zeitgesteuerten Basis, was zu einer Art „kontinuierlichen Integration“ führt.
11.5 Zusammenfassung Die Tour in diesem Kapitel durch die Deployment-Praktiken ist zugegebenermaßen eher Vorgeschmack als vollständige Mahlzeit, doch hoffentlich haben Sie dadurch Appetit auf mehr bekommen. Wir haben uns u. a. deswegen in einem Kapitel mit dem Deployment beschäftigt, weil wir darstellen wollten, dass sich die Produktion eines Qualitätsprodukts nicht nur im Schreiben von qualitativ gutem Code erschöpft. Außerdem sollten Sie die Grundlagen von einigen Ansätzen und Praktiken kennenlernen. Wir wollten auch demonstrieren, wie einige der aktuellen Komponenten des Zend Frameworks wie Zend_Http und Zend_Config in den Prozess eingebunden werden können. Seien Sie versichert, dass die Zahl der Komponenten steigen wird, die speziell auf den Deployment-Aspekt der Entwicklung abzielen. Welche dieser Praktiken Sie schließlich implementieren, wird nicht nur von Ihrer Arbeitsumgebung abhängen, sondern auch von den Anforderungen des Projekts, an dem Sie arbeiten. Letzten Endes hat dieses Kapitel seinen Auftrag erfüllt, wenn Sie einen besseren Überblick über den Prozess bekommen haben und sich ermuntert fühlen, dieses Thema im Detail mehr erforschen zu wollen.
268
III Teil III – Machen Sie Ihre Applikation leistungsfähiger In den Kapiteln 12 bis 16 werden die Komponenten erläutert, mit denen Sie Ihre Applikationen aufpeppen können. Dabei geht es um die Integration von XML-RPC- und RESTTechnologien bis zur Einbindung einer Vielfalt von öffentlichen Webservices, die über die Zend_Service-Komponenten verfügbar sind. Webservices Webservices stellen uns vor Probleme der Netzwerkverfügbarkeit, die durchs Caching wesentlich verringert werden können – auch dies wird neben dem Einsatz von Caching zur Verbesserung der Performance thematisiert. Wenn Sie die Reichweite Ihrer Applikation steigern, gehört dazu oft auch eine internationale Bereitstellung. Von daher beschäftigen wir uns auch mit den Features im Zend Framework, mit denen man eine Website in mehreren Sprachen anbieten kann. Abschließend runden wir das Buch damit ab, dass wir vom Web zum Print wechseln und dafür die Zend Framework-Komponenten einsetzen, die PDF-Dateien erstellen können. Dem Teil III folgen drei Anhänge. Anhang A nimmt Sie mit auf eine kurze Tour durch die PHP-Syntax und richtet sich an jene, die von anderen Programmiersprachen her kommen. Anhang B beschreibt das Objektmodell von PHP5 und greift somit jenen unter die Arme, die vor der Arbeit mit dem Zend Framework vor allem prozedural programmiert haben. Anhang C enthält Tipps und Tricks, mit denen Sie Ihre Zend Framework-Applikationen einfacher entwickeln können.
12 12 Der Austausch mit anderen Applikationen Die Themen dieses Kapitels
Einführung in Webservices Einen RSS-Feed mit Zend_Feed erstellen und verarbeiten Einen Zend_XmlRpc-Server in eine Zend Framework-Applikation integrieren Einen REST-Server mit Zend_Rest erstellen In diesem Kapitel beschäftigen wir uns mit den verschiedenen Komponenten des Zend Frameworks, die man lose unter dem Begriff „Webservices“ zusammenfassen kann. Diese definiert das World Wide Web Consortium (W3C) als „Softwaresystem, das eine interoperable Rechner-zu-Rechner-Interaktion über ein Netzwerk unterstützt“. Aus Gründen der Einfachheit werden wir unsere Verwendung des Begriffs „Webservices“ in diesem Kapitel auf dieser Definition gründen, anstatt auf dem spezielleren Schwerpunkt der Kombination von SOAP und WSDL (Web Services Description Language), den das W3C einnimmt. Zu dieser Interoperabilität gehört zum Teil der Einsatz von XML, doch einer der Vorteile dieser Komponenten ist, dass wir uns nicht auf XML selbst konzentrieren müssen. Das Zend Framework enthält eine Reihe von Tools, die sich um die Formatierung und das Protokoll der Interaktion kümmern. So können Sie sich auf die Logik konzentrieren und brauchen dafür nur PHP. Nehmen Sie sich einfach mal einen Moment Zeit und zählen alle Formate auf, die nur für Newsfeeds im Netz verfügbar sind. Dann wird schnell der Vorteil offensichtlich, sich nicht mit dieser großen Bandbreite an Formaten und deren Änderungsrate herumschlagen zu müssen. Bevor wir uns an den Einsatz der Zend Framework-Komponenten machen, schauen wir uns an, warum wir Applikationen anhand von Webservices integrieren wollen und auch sollten.
271
12 Der Austausch mit anderen Applikationen
12.1 Die Integration von Applikationen Es ist interessant, wie viele Webservices bereits zum Bestandteil unserer On- und OfflineExistenz geworden sind. Jedes Mal, wenn Nick seinen Rechner hochfährt, wird ein Newsreader gestartet, der eine Liste mit XML-formatierten News von Sites abholt, bei denen er ansonsten nie auf dem Laufenden bleiben könnte. Kürzlich hat seine Frau einen ganzen Haufen Flohmarktsachen über GarageSale verkauft. Das ist eine Desktop-Applikation für Mac OS X, die über ihre auf XML basierende API mittels HTTP mit eBay spricht. Der Schlüssel zu all diesen Aktionen ist der Austausch von Informationen über ein Netzwerk und die Distribution der Rechenarbeit.
12.1.1 Austausch strukturierter Daten XML steht für Extensible Markup Language und stammt von der Standard Generalized Markup Language (SGML) ab – einer der vielen Markup- oder Auszeichnungssprachen, deren Rolle einfach darin besteht, Text zu beschreiben. Die wahrscheinlich bekannteste ist HTML (Hypertext Markup Language). Diese Sprache beschreibt Textdokumente, die per HTTP übermittelt werden sollen. Daten müssen nicht ausgezeichnet werden, um ausgetauscht werden zu können, doch in den meisten Fällen brauchen sie in irgendeiner Form eine Struktur. Hier sind einige Beispiele:
Komma- oder tabulatorgetrennte Werte (CSV oder TSV) Strukturierter Text, dessen Struktur auf einer regelmäßigen Datensequenz basiert, die von konsistenten Begrenzern (Delimiter) getrennt werden.
Serialisierte Daten wie solche, die von der PHP-eigenen serialize()-Funktion erstellt werden.
Andere Formate wie die JavaScript Object Notation (JSON), die als Alternative zu XML in Ajax verwendet werden kann (und die im Zend Framework durch Zend_Json geleistet wird). Diese Informationen sollten den Lesern dieses Buches kaum neu vorkommen, doch hier geht’s darum, dass wir Informationen von einem System zu einem anderen übertragen wollen, sodass das empfangende System weiß, wie es mit diesen Daten umzugehen hat. Wenn wir uns in Abbildung 12.1 die Applikation GarageSale anschauen, ist es eindeutig eine ziemlich komplexe Applikation, deren Daten nicht mit anderen ausgetauscht werden könnte, außer sie sind passend strukturiert, damit eBay sie verarbeiten und die jeweiligen Anfragen ausführen kann, z. B. ein neues Element für eine Auktion zu erstellen.
272
12.1 Die Integration von Applikationen
Abbildung 12.1 Die Desktop-Applikation GarageSale unter Mac OS X spricht anhand von Webservices mit der auf XML basierenden API von eBay.
Nach einem Blick auf die Formatierung der an dieser Diskussion beteiligten Daten zwischen den Applikationen lautet die nächste Frage, wie die Applikationen miteinander kommunizieren.
12.1.2 Produktion und Verarbeitung strukturierter Daten E-Mails dienen als gutes Beispiel dafür, wie Applikationen vermittels eines strukturierten Datenformats (Internet-E-Mail-Format) miteinander sprechen können. Dieses Datenformat wird an dem einen Ende produziert, über einen Mailserver (MTA) versendet und dann auf Empfängerseite vom E-Mail-Client (MUA) verarbeitet. Diese „Gespräche“ zwischen den Applikationen, die das Thema dieses Kapitels sind, treiben dieses Grundkonzept einen Schritt weiter, indem durch diesen Austausch auf den jeweiligen Seiten Aktionen ausgelöst werden, und zwar durch einen sogenannten Remote Procedure Call (RPC). Im weiteren Verlauf dieses Kapitels wird es um Zend_XmlRpc gehen, das mit Zend_ arbeitet, um XML-kodierte RPCs über HTTP zu senden und zu empfangen. Ursprünglich geschaffen von Dave Winer, ist XML-RPC eine überraschend einfache und flexible Spezifikation, die deswegen in ganz verschiedenartigen Situationen eingesetzt wird.
XmlRpc_Server
Als weitere Funktionalitäten eingebaut wurden, entwickelte sich XML-RPC zu SOAP (ursprünglich ein Akronym, aber jetzt einfach nur ein Wort), das auch vom W3C adaptiert wurde. Sie brauchen sich nicht lange umzuhören, bis Sie erfahren, zu welchen Beschwerden diese erweiterten Funktionalitäten wegen der höheren Komplexität führten. Das erklärt teilweise, warum trotz der offiziellen Anerkennung von SOAP durch das W3C weiterhin
273
12 Der Austausch mit anderen Applikationen XML-RPC verwendet wird. In mancherlei Hinsicht ist SOAP selbst schon von anderen Protokollen wie Atom abgelöst worden. Auch das Zend Framework spiegelt den Status dieser Protokolle wider. Zend_Soap selbst dümpelte über zwei Jahre im Inkubator herum, ehe es endlich abgeschlossen wurde – das lag weitgehend am mangelnden Interesse der User, während XML-RPC und Atom beide in den Core aufgenommen wurden. Wir schauen uns die Komponenten des Zend Frameworks gleich an, doch vorher soll noch erläutert werden, wie Webservices funktionieren und warum wir damit arbeiten wollen.
12.1.3 Wie Webservices arbeiten Die Einfachheit von Webservices präsentiert man am einfachsten über eine Illustration wie in Abbildung 12.2. Ganz einfach ausgedrückt arbeiten Webservices wie viele andere Methoden zur Datenübermittlung: Daten werden am einen Ende formatiert (z. B. mit XMLRPC ins XML-Format), dann über ein Protokoll übertragen (in diesem Fall HTTP) und schließlich am anderen Ende weiterverarbeitet. Applikationscode
Applikationscode
An Zend_XmlRpc_Server angehängter PHP-Code
Aus Zend_XmlRpc_Server ausgelesener PHP-Code
XML-RPC-Server Zend_XmlRpc_Server
XML-RPC-Server Per HTTP übersandte XML-Daten
Zend_XmlRpc_Client
Abbildung 12.2 Die grundlegende Transaktion eines Webdienstes zwischen zwei Systemen mit XMLRPC
Natürlich ist diese Erklärung so allgemein gehalten, dass sie praktisch wertlos ist. Um also noch besser zu veranschaulichen, wie Webservices arbeiten, wollen wir den Schritten eines XML-RPC-Beispiels folgen, bei dem eine Desktop-Applikation aktualisierte Preise von einem Onlinedienst bezieht: 1. Eine Desktop-Applikation holt die Daten, die für einen Procedure-Aufruf benötigt werden (einschließlich der ID des angeforderten Artikels und der Remote Procedure, die die Preise für die Artikel holt). Die XML-RPC-Client-Komponente der DesktopApplikation kodiert diesen Remote Procedure Call ins XML-Format und sendet ihn wie folgt an den Onlinedienst: <methodCall> <methodName>onlineStore.getPriceForItem <params> <param> 123
274
12.1 Die Integration von Applikationen 2. Der XML-RPC-Server des Onlinedienstes empfängt den XML-kodierten ProcedureAufruf und dekoriert ihn in ein Format, das der Systemcode verarbeiten kann, z. B. $store->getPriceForItem(123). Der Systemcode gibt den angeforderten Preis an den XML-RPC-Server zurück, der dies als XML-Response kodiert und ihn an die anfordernde Desktop-Applikation zurückschickt: <methodResponse> <params> <param> <double>19.95
3. Die Desktop-Applikation empfängt die Antwort, dekodiert sie in ein Format, das sie verarbeiten kann, und aktualisiert den Preis für Artikel 123 auf 19,95 €. So haben Sie eine ungefähre Vorstellung von der Funktionsweise von Webservices , doch die Frage bleibt, warum wir damit arbeiten sollten.
12.1.4 Aufgabengebiete für Webservices Wofür brauchen wir Webservices? Die einfache Antwort ist auch die ironischste: Wir brauchen Webservices, damit Applikationen, die auf unterschiedlichen Plattformen oder Frameworks laufen, miteinander auf standardisierte Weise sprechen können. In diesem Kapitel wurde bereits die Ironie dieses Konzepts angesprochen, indem wir Sie mit einer kleinen Auswahl der verschiedenen Protokolle verwirrt haben, aus denen diese „Standards“ bestehen. Lassen wir die Ironie mal beiseite und kehren zum Beispiel unserer Desktop-Applikation zurück, die aktualisierte Preise von dem Onlinedienst abholt: Ihnen wird auffallen, dass es nur wenige Details darüber gab, wie jedes Ende der Transaktion diese Procedure-Aufrufe tatsächlich durchführt. Der Grund ist, dass es egal ist, denn solange jede Applikation in der Lage ist, ihre internen Prozesse in eine Standardform der Kommunikation umzuwandeln (das XML-RPC-Protokoll), kann die Transaktion abgeschlossen werden. Das kann eindeutig zu sehr leistungsfähigen Interaktionen führen wie z. B. einer solchen zwischen GarageSale und eBay. In diesem Kapitel und dem nächsten werden wir uns mit Beispielen beschäftigen, wie Komponenten des Zend Frameworks einige der Komplexitäten von Webservices umgehen können, damit solche Interaktionen vorteilhaft eingesetzt werden können. Wir beginnen mit einem Beispiel, das den meisten Lesern wahrscheinlich vertraut sein wird: Web-Feeds und die Vorteile von Zend_Feed, um Feeds zu produzieren und zu verarbeiten.
275
12 Der Austausch mit anderen Applikationen
12.2 Die Produktion und Verarbeitung von Feeds mit Zend_Feed Das Online-Manual des Zend Frameworks wird Zend_Feed so beschrieben, dass es „eine Funktionalität für die Verarbeitung von RSS- und Atom-Feeds“ bereitstellt. Wir beginnen am entgegengesetzten Ende dieser Transaktion, indem wir zeigen, wie man damit auch RSS- und Atom-Feeds produzieren kann. Anschließend wird es um die Verarbeitung von Feeds gehen.
12.2.1 Die Produktion eines Feeds Wenn Sie sich der Herausforderung zu Anfang dieses Kapitels gestellt haben, also alle für Web-Feeds verfügbaren Formate aufzulisten, werden Sie sicher mit dem Zweig RDF oder RSS 1.* angefangen haben, der RSS 0.9, 1.0 und 1.1 umfasst. Dann haben Sie wahrscheinlich mit RSS 2.* weitergemacht, wozu RSS 0.91, 0.92 und 2.01 gehören. Von dort aus werden Sie wohl beim Syndikationsformat Atom gelandet sein. Wenn Sie das unter dem Druck machen, einen Abgabetermin einhalten zu müssen, und dann entdecken, dass alle diese Formate aktuell auf Millionen von syndizierten Sites eingesetzt werden, dann wird bei Ihnen sicher der kalte Schweiß ausbrechen! Zum Glück brauchen Sie sich nur die neuesten und größten Formate auszusuchen und sich auf deren Ausgabe zu konzentrieren. Doch nicht einmal das ist nötig, weil sich Zend_Feed für Sie um das Format kümmern kann; Sie brauchen ihm nur die Daten für den Feed zu übergeben. Listing 12.1 demonstriert eine sehr einfache Controller-Action, die ein RSS(2.0) oder Atom-Feed aus den Artikeln in unserer Places-Applikation produziert. Listing 12.1 Eine Controller-Action zur Produktion von Feeds require_once 'Zend/Feed.php'; require_once 'models/ArticleTable.php'; class FeedController extends Zend_Controller_Action { public function indexAction() { $format = $this->_request->getParam('format'); $format = in_array($format, array('rss', 'atom')) ? $format : 'rss'; $articlesTable = new ArticleTable(); $rowset = $articlesTable->fetchAll();
In Listing 12.1 beginnen wir mit der Bestimmung des Feed-Formats, und falls weder das RSS- noch das Atom-Feed-Format angefordert wird, nehmen wir standardmäßig RSS n. Als Nächstes holen wir eine Artikelauswahl aus der Datenbank, die in den Feed eingefügt werden soll (diese wird wahrscheinlich begrenzt, dieses Beispiel ist aber vereinfacht) o. Ein mehrdimensionales Array, das aus dem -Element besteht, wird dann konstruiert p, wobei jedes hinzugefügt wird, indem eine Schleife mit dem aus der Articles-Tabelle ausgelesenen Rowset ausgeführt wird q. Dieses Array wird dann in Zend_Feed importiert – zusammen mit dem Format, indem es kodiert werden soll r. Dann wird es als XML-String ausgegeben und kann gleich von einem Newsfeed-Reader oder Aggregator weiterverarbeitet werden s. Beachten Sie, dass wir die send()-Methode verwenden, die den Inhaltstyp des HTTPHeader-Strings auf etwas wie das Folgende setzen: Content-Type: text/xml; charset=UTF-8
Wenn wir den Feed auf eine andere Weise nutzen würden, könnten wir einfach mit der folgenden Zeile den XML-String ohne die HTTP-Header bekommen: $feed->saveXml()
Weil wir einen XML-String generieren und diesen in einer Controller-Action verwenden wollen, deaktivieren wir die automatische View und das Layout-Rendering t. In Abbildung 12.3 sehen wir, dass Firefox es als Web-Feed erkennt, seinen geparsten Inhalt zeigt und fragt, ob wir es anhand der „Live Bookmarks“ (Dynamische Lesezeichen) abonnieren wollen. Es sollte noch angemerkt werden, dass der produzierte Feed ein wenig zu minimal ist und wir wahrscheinlich noch andere Elemente ergänzen müssen, damit er mit anderen Readern und Aggregatoren funktioniert. Wir haben ihn nur aus Gründen der Klarheit schlicht gehalten.
277
12 Der Austausch mit anderen Applikationen
Abbildung 12.3 So erscheint der produzierte Feed im Firefox-Browser zusammen mit dem XMLQuellcode.
Nach der Produktion eines Feeds mit Zend_Feed fahren wir nun mit dem Auslesen und seiner Verarbeitung fort.
12.2.2 Die Verarbeitung eines Feeds Bei der Arbeit an diesem Kapitel erwähnte Nick, dass man mit Web-Feeds auch eine Verzeichnis-Site ergänzen könnte, und zwar mit News, die von den aufgelisteten Websites ausgelesen werden. In diesem speziellen Fall hatte jedes Listing den URL seines Feeds zusammen mit anderen Daten gespeichert. Wenn diese Site anhand von Zend Framework erstellt worden wäre, wäre das Speichern des speziellen Feed-URL unnötig gewesen, weil Zend_Feed jede beliebige HTML-Seite parsen und nach den gleichen Link-Elementen suchen kann, mit denen moderne Browser das Vorhandensein eines Feeds anzeigen, z. B. in der folgenden Weise:
Dafür ist nur eine einzige Codezeile erforderlich: $feedArray = Zend_Feed::findFeeds('http://places/');
Weil wir den Feed aus dem Beispiel von vorhin verwenden, kennen wir den URL bereits, und der Code, um diesen Feed einzulesen, ist ganz unkompliziert, weil er direkt von diesem URL importiert: $this->view->feed = Zend_Feed::import( 'http://places/feed/index/format/rss/' );
278
12.3 RPCs mit Zend_XmlRpc erstellen Die Elemente dieses Feeds könnten dann wie folgt in einer View präsentiert werden:
Wenn wir hier die Methoden der Verarbeitung von Feeds erläutern, wäre es nachlässig von uns, wenn wir die noch verbleibenden Methoden nicht erwähnten, z. B. den Import aus einer Textdatei: $cachedFeed = Zend_Feed::importFile('cache/feed.xml');
Und hier ist ein Beispiel des Imports aus einer PHP-Stringvariablen: $placesFeed = Zend_Feed::importString($placesFeedString);
Wir haben zwar nicht alle Features von Zend_Feed abgedeckt, aber zumindest jene, mit denen Sie wahrscheinlich meistens arbeiten werden. Nun können wir also getrost mit dem nächsten Abschnitt über Zend_XmlRpc weitermachen.
12.3 RPCs mit Zend_XmlRpc erstellen Wir haben bereits beschrieben, wie XML-RPC in XML kodierte RPCs anhand von HTTPAnfragen und -Antworten erstellt. Da XML relativ jung ist, könnte man glauben, dass dies wieder eine neue Technologie sei, die uns die Marketingabteilungen aufgedrückt haben, aber RPCs sind kein neues Konzept. Vor über dreißig Jahren wurde der RFC 707, „A High-Level Framework for Network-Based Resource Sharing“, geschrieben, in dem das RPC-Protokoll in einer etwas poetischen Weise beschrieben wurde: Bei einem solchen Protokoll könnten die verschiedenen entfernten Ressourcen, mit denen ein User zu arbeiten wünscht, als eine einzige, kohärente Werkstatt erscheinen, indem zwischen Ressourcen und User ein Befehlsspracheninterpreter geschaltet wird, der seine Befehle in die entsprechenden Protokolläußerungen umsetzt. RFC 707, „A High-Level Framework for Network-Based Resource Sharing“, 14. Januar 1976
Zu Anfang des RFC 707 wird das ARPANET (der Vorgänger des heutigen Internets) auf interessante Weise herausgefordert: „In diesem Papier wird eine interessante Alternative zu dem von den Schöpfern des ARPANET-Systems eingeschlagenen Weges skizziert.“ Es wäre zwar auch interessant, sich zu überlegen, wie das Internet heute aussähe, wenn man
279
12 Der Austausch mit anderen Applikationen diesen alternativen Ansatz verfolgt hätte, aber der zentrale Punkt ist, dass RPCs eine Lösung unter vielen sind, einschließlich des Internets selbst, damit grundverschiedene Applikationen miteinander sprechen können. Noch eine weitere Anmerkung: Weil RPC zwischen den verschiedenen Applikationen vermittelt, kann es als Middleware klassifiziert werden, was an sich nicht sonderlich interessant ist, bis Ihnen auffällt, dass auch SQL zu dieser Klassifikation gehört. XML-RPC bringt genug mit, um jeden Vorschlag aufzuwerten: Es basiert auf einer vor über dreißig Jahren etablierten Technologie, die teilweise als Alternative zum heutigen Internet gedacht war, und teilt sich den gleichen Problemlösungsbereich wie die Sprache, mit der wir mit Datenbanken sprechen. Nachdem wir festgestellt haben, dass XML-RPC einen passenden Stammbaum aufweist und in Kombination mit XML auch jung genug ist, um lebendig zu bleiben, können wir mit seiner Implementierung im Zend Framework weitermachen. Das von uns angeführte Beispiel ist eine Implementierung der verschiedenen Blog-APIs, mit denen Blog-Editoren alternative Methoden zum Einfügen, Bearbeiten und Löschen von Blog-Einträgen über Desktop- oder andere Remote-Applikationen nutzen. Wir beginnen, indem wir einen XML-RPC-Server mit Zend_XmlRpc_Server einrichten.
12.3.1 Die Arbeit mit Zend_XmlRpc_Server Mit Zend_XmlRpc_Server implementiert man den einzigen Einstiegspunkt (single point of entry) für XML-RPC-Anfragen, und in dieser Hinsicht verhält es sich sehr ähnlich wie der Front-Controller des Zend Frameworks. Tatsächlich können Sie aus Ihrem XML-RPCServer eine Controller-Action machen, die ihre Anfrage über den Front-Controller empfängt, aber das würde ziemlich viel unnötigen Verarbeitungs-Overhead mit sich bringen. Stattdessen werden wir Teile des Bootstrapping-Prozesses herausnehmen und die Serverfähigkeiten darauf aufbauen. 12.3.1.1 Das Bootstrapping einrichten Wenn Sie die vorigen Kapitel gelesen haben, wird Ihnen bereits klar sein, dass die MVCStruktur des Zend Frameworks sich auf die mod_rewrite-Einstellungen in einer .htaccessDatei verlässt, damit alle Anfragen über eine Front-Controller-Datei wie index.php übergeben werden. Weil der XML-RPC-Server seinen eigenen einzigen Einstiegspunkt hat, müssen wir ihn von dieser rewrite-Regel ausschließen. In Listing 12.2 machen wir das durch Einfügen einer rewrite-Bedingung, die alle Anfragen nach /xmlrpc aus der finalen rewrite-Regel für die Weitergabe der Anfragen an index.php ausschließt.
280
12.3 RPCs mit Zend_XmlRpc erstellen Listing 12.2 Modifizierte rewrite-Regeln, damit Anfragen beim XML-RPC-Server eintreffen können RewriteEngine on RewriteCond %{REQUEST_URI} !^/css RewriteCond %{REQUEST_URI} !^/img RewriteCond %{REQUEST_URI} !^/js RewriteCond %{REQUEST_URI} !^/xmlrpc RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php/$1
Schließt /xmlrpc-Verzeichnis von rewrite-Regel aus
Abbildung 12.4 veranschaulicht eine Alternativlösung, falls Sie so feige wie Nick sind, wenn’s um die geheimnisvolle Kunst von mod_rewrite geht.
.htaccess index.php web_root
xmlrpc .htaccess index.php
Abbildung 12.4 Die Verzeichnisstruktur mit unserer xmlrpc-Controller-Datei
In Abbildung 12.4 haben wir im Verzeichnis /xmlrpc eine .htaccess-Datei mit nur einer Zeile eingefügt: RewriteEngine off
Das bedeutet, wir können das xmlrpc-Verzeichnis in irgendeine der Applikationen stellen, an denen wir arbeiten, und alle Anfragen an /xmlrpc werden an unsere xmlrcp/index.phpDatei gerichtet. So müssen wir uns nicht an einer ansonsten schön getunten .htaccess-Datei zu schaffen machen, die zur Hauptapplikation oder zu mehreren Applikationen gehört, von denen jede jeweils unterschiedliche rewrite-Einstellungen haben könnten. Da Anfragen nun erfolgreich zu index.php gelangen, fügen wir den Code ein, mit denen diese Anfragen an die Bootstrap-Datei im Applikationsverzeichnis weitergeleitet werden. Lesern des vorigen Kapitels wird dieses Setup vertraut vorkommen. Beachten Sie den Aufruf der neuen Methode runXmlRpc() (siehe Listing 12.3). Listing 12.3 Der Inhalt der Datei xmlrpc/index.php include '../../application/bootstrap.php'; $bootstrap = new Bootstrap('general'); $bootstrap->runXmlRpc();
Setzt Konfigurationsabschnitt, der verwendet werden soll
Startet xmlrpc-Funktion im Bootstrap
Nachdem wir nun alles zusammengestellt haben, können wir nun endlich zum Hauptthema dieses Abschnitts kommen: die Arbeit mit Zend_XmlRpc_Server. In Listing 12.4 steht eine gekürzte Version der Bootstrap-Datei, in der die Methode runXmlRpc() enthalten ist, die von index.php aufgerufen worden ist.
281
12 Der Austausch mit anderen Applikationen Listing 12.4 Die Verwendung von Zend_XmlRpc_Server in der Bootstrap-Datei class Bootstrap { public function __construct($deploymentEnvironment) { // Set up any configuration settings here }
Initialisiert die Applikation
public function runXmlRpc() { $server = new Zend_XmlRpc_Server();
Initialisiert XMLRPC-Server Hängt Klassenmethoden als require_once 'Blogger.php'; Bindet Klassendateien require_once 'Metaweblog.php'; XML-RPCein, die an Server require_once 'MovableType.php'; Methodenangehängt werden Handler an $server->setClass('Blogger', 'blogger'); $server->setClass('Metaweblog', 'metaWeblog'); Setzt Zeichen$server->setClass('MovableType', 'mt'); kodierung der $response = $server->handle(); Bearbeitet RPCs Antwort $response->setEncoding('UTF-8'); header( 'Content-Type: text/xml; charset=UTF-8' Setzt ); HTTP-Header echo $response;
} }
Gibt Antwort aus
Wir haben die Inhalte des Konstruktors bei Listing 12.4 absichtlich weggelassen, doch die Methode eingebunden, um zu zeigen, wo wir die Konfigurationseinstellungen eingefügt, die include-Pfade gesetzt und eine Datenbankverbindung aufgebaut sowie andere, für die Applikation spezifische Initialisierungen vorgenommen hätten. Das Einrichten des Servers ist dann ein recht unkomplizierter Vorgang, bei dem das Serverobjekt instanziiert wird, der Server die Klassenmethoden bekommt, die zu seinen Methoden-Handlern werden, die Bearbeitung der XML-RPCs und der Rückgabe der Antwort. Vielleicht ist Ihnen noch aufgefallen, dass wir beim Setzen der Methoden-Handler auch einen Namensraum-String übergeben haben. Die Beispielklassen, mit denen wir arbeiten, demonstrieren sehr gut, warum das wichtig ist: Sowohl die Blogger- als auch die Metaweblog-Klasse enthalten eine editPost()-Methode, die miteinander kollidieren würden, wenn wir nicht fähig wäre, sie als metaWeblog.editPost und blogger.editPost mit ihrem Namensraum aufzurufen. Mit dem fertig eingerichteten Server können wir nun näher auf die Klassenmethoden eingehen, mit denen er arbeiten wird. 12.3.1.2 Die Erstellung der XML-RPC-Methoden-Handler Weil der XML-RPC-Server dazu dient, RPCs zu empfangen, wird unser nächster Schritt darin bestehen, diese Prozeduren zu erstellen. Wie bereits erwähnt, werden wir einige der APIs für verschiedene Blog-Applikationen implementieren, damit eine Desktop-Applikation mit Artikeln aus der Places-Applikation arbeiten kann.
282
12.3 RPCs mit Zend_XmlRpc erstellen Zuerst schauen wir uns die Ecto-Applikation an, die wir hierfür einsetzen werden. In deren Beschreibung steht, es handele sich um einen „Desktop-Blogging-Client für Mac OS X und Windows mit einer Vielzahl von Features, die eine große Bandbreite von WeblogSystemen unterstützen“. Wir haben uns vor allem deswegen für Ecto entschieden, weil es über eine Konsole verfügt und darum sehr praktisch ist fürs Debuggen von XML-RPCTransaktionen, doch das Meiste dessen, was in diesem Abschnitt angesprochen wird, gilt auch für andere ähnliche Applikationen. In Abbildung 12.5 sehen Sie links das Hauptfenster von Ecto, darin eine Liste von Places-Artikeln und rechts das Bearbeitungsfenster.
Abbildung 12.5 Mit dem Desktop-Blogging-Client Ecto führen wir XML-RPC-Anfragen durch (gezeigt wird die Bearbeitung von Inhalten aus Places).
Nachdem wir die Verbindungsdetails eingerichtet und die API ausgewählt haben, mit der die Verbindung aufgebaut werden soll, durchläuft Ecto eine Reihe von Methodenaufrufen, um das Konto einzurichten. Wir haben uns für die MovableType-API entschieden, dennoch sind Methodenaufrufe aus anderen APIs wie blogger.getUsersBlogs und blogger.getUserInfo (gehören zur Blogger-API) oder von metaWeblog.editPost (gehört zur MetaWeblog-API) notwendig. Aus diesem Grunde sind wir genötigt, diese Methoden auch unserem XML-RPC-Server bereitzustellen, und dieses Erfordernis ist ein Anzeichen dafür, dass wir Schnittstellen erstellen müssen. 12.3.1.3 Erstellen der Interfaces Damit wir überhaupt mit einer der bereits erwähnten APIs arbeiten können, muss unsere Applikation die von diesen APIs benötigten Methoden implementieren. Wenn wir uns für eine Beispielmethode wie metaWeblog.editPost entscheiden, lautet die Anforderung, dass sie einen Booleschen Wert zurückgeben und die folgenden Parameter verarbeiten soll: metaWeblog.editPost (postid, username, password, struct, publish)
Wie die Applikation diese Anfrage verarbeitet, hängt von der Applikation selbst ab, doch die Tatsache, dass sie die erforderlichen Methoden implementieren muss, verlangt eindeu-
283
12 Der Austausch mit anderen Applikationen tig nach Objektschnittstellen. Dem PHP-Manual zufolge können Sie mit „Objektschnittstellen Code erstellen, der spezifiziert, welche Methode eine Klasse implementieren muss, ohne definieren zu müssen, wie diese Methoden verarbeitet werden“. Listing 12.5 zeigt eine unserer Schnittstellen, die alle Methoden der MetaWeblog-API etabliert. Listing 12.5 Das Interface der MetaWeblog-API interface Places_Service_Metaweblog_Interface { public function newPost( $blogid, $username, $password, $content, $publish ); public function editPost( $postid, $username, $password, $content, $publish ); public function getPost($postid, $username, $password); public function newMediaObject( $blogid, $username, $password, $struct ); public function getCategories( $blogid, $username, $password ); public function getRecentPosts( $blogid, $username, $password, $numposts ); }
Dieses Interface konnten wir problemlos erstellen, weil es in der MetaWeblog-Spezifikation klar und detailliert ausgeführt wird. Die XML-RPC-APIs Blogger und MovableType sind da etwas fummeliger, weil sie zwar eigentlich als veraltet gelten, aber dennoch immer noch in vielen aktuellen Web- und Desktop-Applikationen eingesetzt werden. Nach dem Erstellen eines Beispiel-Interfaces können wir es in unseren konkreten Klassen verwenden und so gewährleisten, dass diese Klassen sich an die Anforderungen der originalen API halten werden. 12.3.1.4 Erstellen der konkreten Klassen Da unser Original-Screenshot von Ecto in Abbildung 12.5 zeigt, wie ein Artikel auf Places bearbeitet wird, werden wir dieses Thema fortführen: Wir demonstrieren anhand der editPost()-Methode von MetaWeblog sowohl die Verwendung des Interfaces als auch, wie Methoden eingerichtet werden müssen, damit Zend_XmlRpc_Server mit ihnen arbeiten kann. Listing 12.6 zeigt eine gekürzte Version der Metaweblog-Klasse, die sich im models/Verzeichnis befindet. Listing 12.6 Das MetaWeblog-Model implementiert das Interface aus Listing 12.5. include_once 'ArticleTable.php'; include_once 'ServiceAuth.php'; class Metaweblog implements Places_Service_Metaweblog_Interface { protected $_auth; protected $ articleTable;
284
Implementiert MetaweblogInterface
12.3 RPCs mit Zend_XmlRpc erstellen public function __construct()
Instanziiert die von
editPost benötigten Objekte
{
$this->_auth = new ServiceAuth; $this->_articleTable = new ArticleTable(); // Full version would have further code here... } /** * Changes the articles of a given post * Optionally, publishes after changing the post * * @param string $postid * Unique identifier of the post to be changed * @param string $username Definiert * Login for a user who has permission to edit the * given post (either the original creator or an erforderliche * admin of the blog) Methoden* @param string $password Password for said parameter * username * @param struct $struct New content of the post * @param boolean $publish * If true, the blog will be published Definiert * immediately after the post is made Methodenrückgabewert * @return boolean */ public function editPost($postid, $username, Definiert Parameter, $password, $struct, $publish die zu DocBlocks
das in aktualisierter $filterStripTags = new Zend_Filter_StripTags; $data = array( Abfrage verwendet wird 'publish' => (int)$struct['publish'], 'title' => $filterStripTags->filter($struct['title']), 'body' => $struct['description'] ); $where = $this->_articleTable->getAdapter() ->quoteInto('id = ?', (int)$postid); $rows_affected = $this->_articleTable Aktualisiert Posting ->update($data, $where); if(0 == $rows_affected) { throw new Exception('Your post failed to be updated'); } return true; Gibt bei Erfolg Booleschen Wert zurück
} // Add all the other required methods here... }
Diese Model-Klasse implementiert das Metaweblog-Interface n, also müsste die finale Version alle Methoden im Interface einbinden, doch aus Platzgründen werden sie hier nicht aufgeführt. Entsprechend werden nur die Objekte im Konstruktor instanziiert, die für die editPost()-Methode erforderlich sind o, doch bei der kompletten Version würden mehr gebraucht.
285
12 Der Austausch mit anderen Applikationen DocBlocks sind ganz wesentlich, wenn Methoden mit Zend_XmlRpc_Server zusammenarbeiten sollen. Über deren Bedeutung werden wir uns in diesem Abschnitt noch näher auslassen, doch ihre zentrale Rolle liegt darin, den Methodenhilfetext und die Methodensignaturen zu bestimmen. In unserem Beispiel können Sie sehen, dass sie Typ, Variablennamen und die Beschreibung aller Parameter p und den Rückgabewert anzeigen q. Die Parameter unserer Methode müssen dann zu denen im DocBlock passen r, obwohl unser Interface auch die Parameter erzwingen wird. Wegen der Sicherheit nehmen wir eine rudimentäre Autorisierungsprüfung vor und verwenden dafür den in den Parametern mit einer eigenen auth-Klasse übergebenen Usernamen und das Passwort s. Falls das nicht klappt, werfen wir eine Exception. Zum Erstellen des Arrays verwendete Daten werden in der Update-Abfrage verwendet und wo nötig auch gefiltert t. Wenn alles gut geht, aktualisieren wir die Datenbankzeile, die mit der angegebenen ID korrespondiert u, und geben (wie im Rückgabewert des DocBlocks festgelegt) einen Booleschen Wert zurück. Nach einer einfachen Authentifizierungsprüfung anhand des in den Parametern übergebenen Passworts und Usernamens filtert und formatiert diese editPost()-Methode ein Array aus den empfangenen Daten und aktualisiert bei Erfolg die Datenbankzeile v, die mit der angegebene ID korrespondiert, oder ruft bei Fehlschlagen eine Exception auf, um die sich Zend_XmlRpc_Server kümmert. Sie werden bemerkt haben, dass sich die editPost()-Methode nur minimal von einer Standardmethode unterscheidet, und zwar insofern, dass sie den DocBlock-Parameterdatentyp struct hat, der kein nativer PHP-Datentyp ist. Wenn Sie Zend_XmlRpc_Server:: setClass() oder Zend_XmlRpc_Server::addFunction()verwenden, prüft Zend_Server_ Reflection alle Methoden oder Funktionen und bestimmt anhand der DocBlocks deren Methodenhilfetext und Signaturen. In Tabelle 12.1 können wir sehen, dass der Datentyp im Fall von @param $struct auf den XML-RPC-Typ struct gesetzt wurde. Dieser entspricht dem PHP-Typ des assoziativen Arrays, das anhand des Zend_XmlRpc_Value_Struct-Objekts verarbeitet wird. Tabelle 12.1 PHP-Typen den entsprechenden XML-RPC-Typen und Zend_XmlRpc_Value-Objekten zuordnen
12.3 RPCs mit Zend_XmlRpc erstellen Nativer PHP-Typ
XML-RPC-Typ
Zend_XmlRpc_Value-Objekt
Array
<array>
Zend_XmlRpc_Value_Array
Assoziatives Array
<struct>
Zend_XmlRpc_Value_Struct
Mit diesen Mappings können wir jederzeit ein Zend_XmlRpc_Value-Objekt aufrufen, um Werte für XML-RPC vorzubereiten. Das wird oft benötigt, wenn Werte in einem Array vorbereitet werden, z. B.: array('dateCreated' => new Zend_XmlRpc_Value_DateTime( $row->date_created, Zend_XmlRpc_Value::XMLRPC_TYPE_DATETIME);
Das vorige Beispiel formatiert ein Datum aus einer Datenbank-Tabellenzeile in das bei XML-RPC erforderliche Format ISO8601. Der zweite Parameter bezieht sich auf die Klassenkonstante XMLRPC_TYPE_DATETIME, die nicht überraschend als dateTime.iso8601 definiert wird. Wenn Sie meinen, dass all diese Introspektion von Zend_Server_Reflection auch ihren Preis hat, dann liegen Sie richtig, vor allem, wenn an den Server viele Klassen oder Funktionen angehängt sind. Glücklicherweise gibt es eine Lösung in Form von Zend_XmlRpc_Server_Cache, der, wie der Name schon sagt, zum Cachen der von Zend_XmlRpc_Server_Reflection gesammelten Informationen verwendet wird. Wir brauchen an dem im Zend Framework-Manual angegebenen Beispiel nicht sonderlich viel zu ändern, weil Zend_XmlRpc_Server_Cache sehr einfach zu verwenden ist (siehe Listing 12.7). Listing 12.7 Der Zend_XmlRpc_Server mit implementiertem Caching $cacheFile = ROOT_DIR . '/cache/xmlrpc.cache'; $server = new Zend_XmlRpc_Server(); if (!Zend_XmlRpc_Server_Cache::get( $cacheFile, $server) ) { require_once 'Blogger.php'; require_once 'Metaweblog.php'; require_once 'MovableType.php';
Da Zend_XmlRpc_Server_Cache nun arbeitsbereit ist, können wir all die ressourcenintensive Introspektion ausblenden. Wenn Code in einer der angehängten Klassen geändert werden muss, müssen wir nur die Cache-Datei löschen, damit eine neue Version geschrieben werden kann, die die Änderungen berücksichtigt. Nach Einrichten des XML-RPC-Servers können wir uns die Client-Seite des Austauschs anschauen: Zend_XmlRpc_Client.
287
12 Der Austausch mit anderen Applikationen
12.3.2 Die Arbeit mit Zend_XmlRpc_Client Bei der Places-Applikation war es unser Anliegen, einen XML-RPC-Server einzurichten, damit wir die Artikel auch remote bearbeiten können, und zwar einer der DesktopApplikationen, die die Blog-APIs unterstützen. Damit haben wir gleichzeitig eine Menge der Funktionalität von Zend_XmlRpc abgedeckt. In diesem Abschnitt werden wir diese Funktionen noch ein wenig ausführlicher demonstrieren, indem wir mit Zend_XmlRpc_Client eine Client-Anfrage an die editPost()-Methode simulieren, die wir schon in Listing 12.6 demonstriert haben. Anders als der Zend_XmlRpc_Server, der mit einem eigenen Front-Controller eingesetzt wurde, arbeiten wir mit Zend_XmlRpc_Client aus einer Controller-Action heraus (siehe Listing 12.8). Listing 12.8 Die Nutzung von Zend_XmlRpc_Client in einer Controller-Action public function editPostAction() Weist Server-URL { an XML-RPC-Server zu $xmlRpcServer = 'http://places/xmlrpc/'; $client = new Zend_XmlRpc_Client($xmlRpcServer);
Daten für XML-RPC struct werden eingerichtet und gefiltert
'title' => $filterStripTags->filter($_POST['title']), 'dateCreated' => new Zend_XmlRpc_Value_DateTime(time(), Zend_XmlRpc_Value::XMLRPC_TYPE_DATETIME), 'description' => $filterStripTags->filter($_POST['body'] ); $server = $client->getProxy(); $server->metaWeblog->editPost( Nimmt RPC array($id,'myusername','mypassword', mit Daten vor $structData,$publish));
Richtet ServerProxy-Objekt ein
}
In diesem Beispiel filtern wir die Daten aus einem HTML-Formular und bereiten sie zum Einsatz mit dem Client vor, der mit dem URL des XML-RPC-Servers belegt wurde, an den wir die Anfragen richten. Das Server-Proxy-Objekt Zend_XmlRpc_Client kümmert sich dann um den Rest, indem die XML-kodierte Anfrage kompiliert und dann über HTTP an den XML-RPC-Server gesendet wird, mit dem es instanziiert wurde. Leihen wir uns das Konsolenfenster von Ecto aus, um in Abbildung 12.6 diesen Prozess gründlicher zu illustrieren. Dort sehen wir die XML-kodierte Anfrage im linken Konsolenfenster, die vom XML-RPC-Server verarbeitet wird. Ist sie erfolgreich, wird der Artikel in der Mitte aktualisiert und eine XML-kodierte Antwort mit dem Booleschen Wert true an den Client zurückgegeben.
288
12.4 Die Nutzung von REST-Webservices mit Zend_Rest
Abbildung 12.6 Eine Demonstration des Methodenaufrufs editPost() mit der Anfrage links, dem aktualisierten Artikel in der Mitte und der vom XML-RPC-Server zurückgegebenen Antwort rechts Natürlich sind nicht alle XML-RPC-Anfragen erfolgreich, und aus diesem Grund besitzt der Zend_XmlRpc_Client die Fähigkeit, anhand von Exceptions mit HTTP- und XMLRPCFehlern umzugehen. In Listing 12.9 fügen wir dem Procedure-Aufruf editPost() aus Listing 12.8 die Fehlerbehandlung hinzu. Listing 12.9 Einfügen der Fehlerbehandlung in den XML-RPC-Client try { client->call('metaWeblog.editPost', array($id,'myusername','mypassword', $structData, $publish)); } catch (Zend_XmlRpc_HttpException $e) { // echo $e->getCode() . ': ' . $e->getMessage() . "\n"; } catch (Zend_XmlRpc_FaultException $e) { // echo $e->getCode() . ': ' . $e->getMessage() . "\n"; } Versucht einen RPC Verarbeitet HTTP-Anfragefehler, falls Aufruf fehlschlägt Verarbeitet alle XML-RPCFehler, falls kein HTTP Unser Client versucht nun, den RPC zu machen, und kann beim Fehlschlagen mit HTTPund XML-RPC-Fehlern umgehen. Beachten Sie, dass wir hier nicht näher darauf eingehen, was wir mit diesen Fehlermeldungen machen könnten, weil wir diesen Client gar nicht in der Places-Applikation einsetzen. Wie in diesem Abschnitt demonstriert, ist XML-RPC recht unkompliziert einzurichten und zu verwenden, und mit Zend_XmlRpc wird das alles sogar noch einfacher. Doch wie jede andere Technologie hat auch XML-RPC ihre Kritiker, und im nächsten Abschnitt schauen wir uns einen anderen Ansatz an, um Webservices mit Zend_Rest zu nutzen.
289
12 Der Austausch mit anderen Applikationen Aussage, dass „REST-Webservices mit dienstspezifischen XML-Formaten arbeiten“, was korrekt ist, aber einiger Erläuterung bedarf, weil es den REST-Webservices egal ist, ob sie mit XML arbeiten oder nicht. Zend_Rest verwendet tatsächlich XML als Format der Wahl. Aber das kann man auch umgehen, wenn man ein wenig herumprobiert. Das machen wir, nachdem wir uns REST etwas näher angeschaut haben.
12.4.1 Was ist REST? REST steht für Representational State Transfer und wurde ursprünglich in „Architectural Styles and the Design of Network-based Software Architectures“ von Roy Fielding skizziert, dessen Rolle als „wichtigster Architekt des aktuellen Hypertext Transfer Protocol“ etwas über den Hintergrund von REST aussagt. Fielding schreibt in dem Dokument: „REST ignoriert die Details der Implementierung von Komponenten und Protokollsyntax, um sich auf die Rolle der Komponenten, die Beschränkungen für deren Interaktion mit anderen Komponenten und ihre Interpretation von signifikanten Datenelementen zu konzentrieren.“ Er stellt außerdem fest, dass „die Motivation für die Entwicklung von REST war, ein architektonisches Modell dafür zu schaffen, wie das Internet arbeiten soll, damit es als führendes Framework für die Standards der Webprotokolle dienen kann“. Anders gesagt: Sobald wir eine HTTP-Anfrage vornehmen, nutzen wir ein auf dem RESTKonzept beruhendes Transferprotokoll. Während das zentrale Element von RPC der Befehl ist, auf den normalerweise über einen einzelnen Einstiegspunkt zugegriffen wird, ist das zentrale Element bei REST die Ressource. Dafür wäre die Login-Seite von Places ein gutes Beispiel, deren Ressourcenidentifikator der URL http://places/auth/login/ ist. Ressourcen können sich im Laufe der Zeit ändern (bei dieser Login-Seite könnte zum Beispiel das Interface aktualisiert werden), doch die Ressourcenidentifikatoren sollten valide bleiben. Natürlich ist es nicht von sonderlich hohem Wert, wenn man eine Menge Ressourcen besitzt, aber mit ihnen nichts machen kann. Im Fall der Webservices haben wir die HTTPMethoden. Um den Wert solch offensichtlich einfacher Operationen zu verdeutlichen, vergleicht die Tabelle 12.2 die HTTP-Methoden POST, GET, PUT und DELETE mit den üblichen allgemeinen Datenbankoperationen: Erstellen, Lesen, Aktualisieren und Löschen (also create, read, update und delete (CRUD)). Tabelle 12.2 Ein Vergleich von bei REST verwendeten HTTP-Methoden mit allgemein üblichen Datenbankoperationen
290
HTTP-Methoden
Datenbankoperationen
POST
Create
GET
Read
PUT
Update
DELETE
Delete
12.4 Die Nutzung von REST-Webservices mit Zend_Rest Allen Lesern dieses Buches werden wahrscheinlich die HTTP-Anfragemethoden POST und GET vertraut sein, während PUT und DELETE wohl eher weniger bekannt sind. Zum Teil liegt das daran, dass sie oft nicht von allen HTTP-Servern implementiert werden. Dieser Beschränkung wird in Zend_Rest entsprochen, weil Zend_Rest_Client zwar alle diese Anfragemethoden senden kann, aber der Zend_Rest_Server nur auf GET und POST reagieren wird. In der Annahme, dass die Leser bereits genug über HTTP wissen, um sich aus diesen Anhaltspunkten ein Gesamtbild zu machen, haben wir diese Vorstellung von REST absichtlich kurz gehalten. Wir hoffen, dass die Dinge klarer werden, wenn wir einige der RESTKomponenten des Zend Frameworks näher untersuchen. Wir beginnen mit dem Zend_Rest_Client.
12.4.2 Die Arbeit mit Zend_Rest_Client Wie bereits zu Beginn dieses Abschnitts erwähnt, arbeitet Zend_Rest mit XML, um die zu verarbeitenden Daten im Body der HTTP-Anfrage oder -Antwort zu serialisieren, doch nicht alle REST-Webservices nutzen XML. Ein Beispiel ist der Spamfilterdienst Akismet: Daran werden wir die verschiedenen Möglichkeiten demonstrieren, wie man auf RESTbasierte Webservices zugreifen kann. In Abbildung 12.7 wird gezeigt, wo wir den Dienst nutzen können, um die von den Usern der Places-Applikation übermittelten Rezensionen zu prüfen, damit Spam sicher draußen bleibt.
Abbildung 12.7 Die Rezensionen in der Places-Applikation, die wir mit dem Spamfilterdienst Akismet filtern können
291
12 Der Austausch mit anderen Applikationen Ein weiterer Grund, warum wir uns für Akismet entschieden haben: Das Zend Framework besitzt eine Zend_Service_Akismet-Komponente, d. h. wir müssen es hier nicht bei einer teilweise implementierten Lösung belassen, und Sie werden sicherlich in der Lage sein zu verstehen, wie diese Komponente funktioniert. In der Akismet-API gibt es eine kleine Auswahl an Ressourcen (siehe Tabelle 12.3), aus der wir die Ressource wählen, um zu verifizieren, dass der API-Schlüssel valide ist, der für die Verwendung von Akismet nötig und von Wordpress.com bezogen wird. Tabelle 12.3 Die in der Akismet-API enthaltenen Ressourcen Ressourcenidentifikator
Beschreibung
rest.akismet.com/1.1/verify-key
Überprüft den erforderlichen APISchlüssel.
api-key.rest.akismet.com/1.1/comment-check
Stellt fest, ob es sich bei dem übermittelten Kommentar um Spam handelt oder nicht.
api-key.rest.akismet.com/1.1/submit-spam
Übermittelt übersehenen Spam
api-key.rest.akismet.com/1.1/submit-ham
Übermittelt Inhalte, die unkorrekterweise als Spam gekennzeichnet wurden.
Als Erstes werden wir nun versuchen, mit Zend_Rest auf diese Ressource in der im Manual demonstrierten Methode zuzugreifen. Das sehen Sie in Listing 12.10. Listing 12.10 Eine REST-Anfrage über Zend_Rest_Client an Akismet senden $client = new Zend_Rest_Client( Client mit URL der 'http://rest.akismet.com/1.1/verify-key' Ressource instanziieren ); echo $client->key('f6k3apik3y') Schlüssel auf ->blog('http://places/') blog auf URL Wert des API->post();
Anfrage als HTTP senden
des Blogs setzen
Schlüssels setzen
In diesem Beispiel konstruieren wir den Client mit dem URL der verify-key-Ressource. Dann setzen wir anhand des Fluent-Interfaces, das in vielen Zend FrameworkKomponenten vorhanden ist, die beiden erforderlichen Variablen key und blog und nutzen dafür die integrierten magischen Methoden. Schließlich senden wir sie über eine HTTPPOST-Anfrage. Leider schlägt trotz der angenehmen Schlichtheit dieses Codes die resultierende Anfrage fehl, weil Akismet nicht die vom Zend_Rest_Client erwartete XMLAntwort zurückgeben wird, wenn es auf diese Weise eingesetzt wird. Wenn Akismet nur eine einfache HTTP-Anfrage braucht und Zend_Rest selbst Zend_ Http_Client verwendet, warum können wir dann nicht gleich einfach Zend_Http_Client nehmen? Die in Listing 12.11 demonstrierte Antwort lautet: Können wir tun.
292
12.4 Die Nutzung von REST-Webservices mit Zend_Rest Listing 12.11 Eine REST-Anfrage an Akismet über Zend_Http_Client senden $client = new Zend_Http_Client( Instanziiert Client 'http://rest.akismet.com/1.1/verify-key' mit Ressourcen-URL ); $data = array( Erstellt Array mit erforderlichen 'key' => 'f6k3apik3y', Daten für Verifizierungsanfrage 'blog' => 'http://places/' ); $client->setParameterPost($data);
wird nicht versuchen, die von Akismet zurückgegebene Antwort so zu parsen, als wäre sie XML, und wir empfangen abhängig davon, ob die Daten erfolgreich verifiziert wurden oder nicht, die Antwort in einfachem Text als valid oder invalid. Zend_Http_Client
Wenn Zend_Http_Client die Anfrage erfolgreich durchführt, gibt es doch sicher einen Weg, damit Zend_Rest das Gleiche machen kann, oder? Natürlich gibt es den: In Listing 12.12, das mittlerweile vertraut vorkommen sollte, umgehen wir, dass die Antwort von Akismet in einfachem Text als XML geparst wird, indem wir die restPost()-Methode direkt aufrufen. Anders als bei unserem ersten Versuch gibt diese Methode den Body der HTTP-Antwort zurück, anstatt ihn durch Zend_Rest_Client_Result zu schicken. Listing 12.12 Eine REST-Anfrage an Akismet über Zend_Rest senden $client = new Zend_Rest_Client( Client mit Anfrage'http://rest.akismet.com' URL instanziieren ); $data = array( Erstellt Array mit erforderlichen 'key' => 'f6k3apik3y', Daten für Verifizierungsanfrage 'blog' => 'http://places/' ); try { $response = $client->restPost( Stellt HTTP'/1.1/verify-key', $data POST-Anfrage ); var_dump($response); } catch (Zend_Rest_Client_Exception $e) { echo $e->getCode() . ': ' . $e->getMessage() . "\n"; }
Nach Lösung unseres Problems mit dem Akismet-Dienst wissen wir nun, dass wir Zend_Rest_Client mit auf einfachem Text und auf XML basierenden REST-ful Webservices nutzen können. Wenn wir mit den restlichen Akismet-Ressourcen arbeiten wollten, wäre es offensichtlich sinnvoller, Zend_Service_Akismet zu nehmen. Doch wenn es keine bereits vorab erstellte Zend Framework-Komponente gäbe, hätten wir verschiedene andere
293
12 Der Austausch mit anderen Applikationen Optionen. Eine davon ist, über Zend_Rest_Server mit auf REST basierenden Webservices zu interagieren, die von unserem eigenen Applikationsserver bereitgestellt werden.
12.4.3 Die Arbeit mit Zend_Rest_Server Stellen wir uns einmal vor, dass wir einen der Anzeigenkunden auf Places, nämlich den Edinburgher Zoo, dazu überreden konnten, an einer gemeinsamen Werbeaktion teilzunehmen, die separat auf unseren beiden Sites gehostet wird. Die Idee dahinter ist, eine Mashup-Site mit kurzer Lebensdauer zu schaffen, die aus den Inhalten von Places, dem Zoo in Edinburgh und anderen interessanten Sites besteht. Ähnlich wie beim XML-RPC-Server beginnen wir, indem wir ein einfaches Interface namens Places_Service_Place_Interface erstellen, damit unser Server eine konsistente API hat. Listing 12.13 zeigt das Interface mit zwei Methoden: eine, um einen Zielort zu holen, und die andere, um Rezensionen für einen Zielort zu holen. Listing 12.13 Das Applikations-Interface für unseren Places-Dienst interface Places_Service_Place_Interface { public function getPlace($id); public function getReviews($id); }
In Listing 12.14 implementieren wir unser Interface konkret anhand solcher Abfragen, die denen aus Kapitel 6 ähneln. Beachten Sie, dass die Datenbankresultate als Array anstatt als Standardobjekte zurückgegeben werden, was zu einem Fehlschlag geführt hätte, wenn sie von Zend_Rest_Server verarbeitet worden wären. Ihnen ist wahrscheinlich außerdem aufgefallen, dass es anders als Zend_XmlRpc_Server bei Zend_Rest_Server nicht erforderlich ist, dass Parameter und Rückgabewerte in DocBlocks spezifiziert werden, auch wenn Zend_Rest_Server sie verwenden wird, falls sie vorhanden sind. Listing 12.14 Die konkreten Klassen des Places-Dienstes class ServicePlaces implements Places_Service_Place_Interface { public function getPlace($id) { $placesFinder = new Places(); $place = $placesFinder->find($id); return $place->current()->toArray(); } public function getReviews($id) { $reviewsFinder = new Reviews(); $rowset = $reviewsFinder->fetchByPlaceId($id); return $rowset->toArray(); } }
294
Gibt Abfrageergebnisse als Array zurück
12.4 Die Nutzung von REST-Webservices mit Zend_Rest Nachdem wir nun die konkrete Klasse haben, können wir sie auf die gleiche Weise wie bei Zend_XmlRpc_Server an Zend_Rest_Server anhängen. Listing 12.15 zeigt den RESTServer, der mit einem Action-Controller eingerichtet und über einen HTTP-GET oder -POST erreichbar ist. Dabei muss der Name der Dienstmethode angegeben sein, die Sie aufrufen wollen. Listing 12.15 Unser REST-Server class RestController extends Zend_Controller_Action { protected $_server; public function init() { $this->_server = new Zend_Rest_Server(); $this->_helper->viewRenderer->setNoRender(); } public function indexAction() { require_once 'ServicePlaces.php'; $this->_server->setClass('ServicePlaces'); $this->_server->handle(); } }
Abbildung 12.8 zeigt die XML-formatierten Ergebnisse einiger beispielhafter GETAnfragen mit Firefox an die Ressourcen http://places/rest/?method=getPlace&id=6 (links) und http://places/rest/?method=getReviews&id=6 (rechts).
Abbildung 12.8 Die Ergebnisse unserer REST-Serverabfrage: getPlace links und getReviews rechts
Unsere Mashup-Site braucht einfach nur Zend_Rest_Client zu verwenden, um Anfragen wie die folgende durchzuführen: $client = new Zend_Rest_Client('http://places/rest/?method=getPlace'); $response = $client->id('6')->get();
295
12 Der Austausch mit anderen Applikationen Damit wird ein Zend_Rest_Client_Result-Objekt zurückgegeben, das es uns erlaubt, auf die Elemente der Antwort als Eigenschaften zuzugreifen, die auf unserer Site wie folgt verwendet werden können: echo $response->name; // Gibt "Edinburgh Zoo" aus
Nach Durcharbeiten der Implementierung von Zend_XmlRpc_Server (was im Vergleich zu beispielsweise SOAP relativ einfach ist) werden Sie merken, dass Zend_Rest_Server sehr einfach nachzuvollziehen ist. So wie bei jeder kurzen Einführung bleibt eine Menge übrig, das noch nicht angesprochen wurde, z. B. die HTTP-Anfragen PUT und DELETE, die nicht vom Zend_Rest_Server bearbeitet werden, und die Authentifizierung. Allerdings haben wir einen REST-Server eingerichtet, deren Stärke in der Beziehung zwischen Zend_Rest_ Server und Zend_Rest_Client und der Einfachheit der Implementierung liegt.
12.5 Zusammenfassung In diesem Kapitel nahmen wir einen sehr konzentrierten Blick auf einige Komponenten für Webservices aus dem Zend Framework. Der Schwerpunkt lag auf den Beziehungen zwischen Client und Server, die wir zuerst über Zend_Feed eingerichtet haben, um einen Newsfeed unserer Places-Artikel zu generieren und diesen dann zu verarbeiten. Als Nächstes richteten wir Zend_XmlRpc_Server ein, um die Places-Artikel auch remote über die Blog-APIs bearbeiten zu können, und dann stellten wir ein Beispiel vor, wie man mit Zend_XmlRpc_Client die RPCs an diesen Server durchführen kann. Schließlich ging es noch um Zend_Rest, wobei als Erstes anhand von Zend_Rest_Client des auf REST basierenden Spamfilterdienstes Akismet die Kommentare gefiltert wurden. Dann stellten wir mit Zend_Rest_Server unsere eigene API zu Places-Inhalten bereit. Hoffentlich beenden Sie dieses Kapitel mit einem guten Verständnis dafür, wie Webservices arbeiten, und können nun anhand der verschiedenen Zend Framework-Komponenten hilfreiche Dienste nutzen. Sie sollten jetzt auch gut aufs nächste Kapitel vorbereitet sein, in dem wir die spezifischeren Komponenten mit einigen der öffentlich verfügbaren Webservices nutzen.
296
13 13 Mashups mit öffentlichen Webservices Die Themen dieses Kapitels
Integration von Zend_Service_Amazon in eine Zend Framework-Applikation Bilder aus Flickr darstellen YouTube-Videos auf der eigenen Website präsentieren Im vorigen Kapitel haben wir uns einige allgemeine Komponenten für Webservices aus dem Zend Framework angeschaut und die Rollen von Client und Server untersucht. In diesem Kapitel nehmen wir uns die Client-Rolle vor und nutzen einige öffentlich verfügbare Webservices, um die Places-Site signifikant aufzuwerten. Es ist auch nicht ganz verkehrt zu sagen, dass dieses Kapitel einfacher und wenn möglich sogar ein wenig spannender ist als das vorige. In Kapitel 12 wurde der Einsatz eines solchen öffentlichen Webservice anhand der allgemeineren Komponenten Zend_Http_Client und Zend_Rest gezeigt: dem Spamfilter Akismet. Dabei erwähnten wir auch, dass es dafür im Zend Framework eine Komponente gibt, nämlich Zend_Service_Akismet. Natürlich bräuchten wir dieses Kapitel gar nicht schreiben, gäbe es da nicht noch mehr von diesen Komponenten. Darum beginnen wir unverzüglich mit einer Übersicht der vielen aktuell verfügbaren Komponenten für Webservices. Anschließend demonstrieren wir den Einsatz einiger Komponenten. Dabei soll nicht nur die allgemeine Nutzung, sondern speziell auch die Integration in eine auf dem Zend Framework basierende Applikation gezeigt werden. Bei diesem Prozess geben wir Ihnen auch vorab schon mal eine Einführung in die Arbeit mit dem Zend_Cache, um die Ergebnisse zu cachen und die lästigen Verzögerungen zu vermeiden, wenn man Inhalte aus einem entfernten Webservice einbindet.
297
13 Mashups mit öffentlichen Webservices
13.1 Der Zugriff auf öffentliche Webservices Durch Konstruktion einer eigenen API aus dem letzten Kapitel, damit die Bearbeiter von Desktop-Blogs auf die Places-Webapplikation zugreifen können, sollte Ihnen das Konzept der APIs schon recht vertraut sein. Dieses Wissen ist hier sehr praktisch, wenn es um die APIs für öffentliche Webservices geht. Allerdings ist es bei den Zend_Service_*Komponenten ganz ausgezeichnet, dass Sie sich nicht sonderlich in die APIs einarbeiten müssen, um sie ans Laufen zu kriegen. Die Zahl der Zend_Service_*-Komponenten nimmt in beträchtlichem Tempo zu. Das unterstreicht die Bedeutung und die Aufmerksamkeit, die der Einbindung von Webservices im Zend Framework gegeben wird. Abgesehen von den gleich vorzustellenden Komponenten gibt es auch viele, die sich in unterschiedlichen Phasen der Fertigstellung befinden, da die Zahl der Webservices steigt. Die folgende Liste enthält alle, die im Zend Framework Core enthalten sind, während wir dies schreiben, und einige, die sich in der Entwicklung befinden und sicher öffentlich angeboten werden, wenn Sie dies hier lesen. Es ist sehr inspirierend, die Vielzahl der Webservices und der sie anbietenden Unternehmen zu sehen – das reicht von sehr großen bis zu vergleichsweise kleinen Firmen: Zend_Gdata Zend_Service_Akismet Zend_Service_Amazon Zend_Service_Audioscrobbler Zend_Service_Delicious Zend_Service_Flickr Zend_Service_Gravatar Zend_Service_Nirvanix Zend_Service_RememberTheMilk Zend_Service_Simpy Zend_Service_SlideShare Zend_Service_StrikeIron Zend_Service_Technorati Zend_Service_Yahoo
Bevor wir uns daran machen, einige dieser Komponenten zu demonstrieren, beschreiben wir sie kurz und geben ein paar Hinweise, wie man sie in Relation zu Places möglicherweise verwenden kann. So wird hoffentlich deutlich, über welches Potenzial diese Dienste verfügen.
13.1.1 Zend_Gdata Man sollte eigentlich erwarten, dass dieser Client für Google Data Zend_Service_Gdata heißt. Aber aus historischen Gründen hält sich die Komponente Zend_Gdata nicht an das Namensschema. Das Google-Entwicklerteam zeichnet für die Zusammenstellung dessen
298
13.1 Der Zugriff auf öffentliche Webservices verantwortlich, was nun zum offiziellen PHP5-Client für die Google Data-APIs geworden ist. Die Tatsache, dass sie als separater Standalone-Download erhältlich ist, verdeutlicht die Stellung der Gdata-Komponente als Teil des Frameworks. Wir werden später ein Beispiel für den Einsatz von Zend_Gdata mit einem der Services von Google geben, doch vorher schauen wir kurz ins Unternehmensprofil von Google: Das Ziel von Google besteht darin, die auf der Welt vorhandenen Informationen zu organisieren und allgemein zugänglich und nutzbar zu machen. http://www.google.de/corporate/index.html
Das sagt uns als potenziellen Usern der APIs von Google nicht sonderlich viel darüber, was diese Dienste können. Mit Blick auf Tabelle 13.1 erkennen wir den Grund: Google bietet über die Google Data-API recht viele Dienste an. Tabelle 13.1 Die über die Data API von Google verfügbaren Dienste Webservice
Einsatzzweck
Google Kalender
Verwaltet online eine Kalenderanwendung.
Google Text & Tabellen
Verwaltet online eine Tabellenkalkulation und Texte.
Google Documents List
Verwaltet Textdokumente, Tabellenkalkulationen und Präsentationen (auch Google Docs genannt)
Google Provisioning
Verwaltet Benutzerkonten, Nicknames und E-Mail-Listen auf einer von Google Apps gehosteten Domäne.
Google Base
Verwaltet online eine Datenbankanwendung.
YouTube
Verwaltet online eine Videoanwendung.
Picasa
Verwaltet online eine Fotoanwendung.
Google Blogger
Verwaltet eine Blogging-Applikation (die aktuelle Inkarnation der Blogger-API, die wir in Kapitel 12 besprochen haben).
Google CodeSearch
Durchsucht den Quellcode öffentlicher Projekte.
Google Notebook
Zeigt öffentliche Notizen und gesammelte Informationen von Webseiten.
Die API Google Data ist der Sammelname für Dienste, die auf dem Atom-Syndikationsformat APP (Atom Publishing Protocol) beruhen. Das erklärt, warum Dienste wie Google Maps (mit dem Sie Kartenmaterial anhand von JavaScript in Webapplikationen einbinden können) nicht in diese Auswahl aufgenommen wurden. Anmerkung
Weil die Zend_Gdata-Komponenten erweiterte Versionen von Atom sind, könnten sie theoretisch auch als generische Atom-Komponenten verwendet werden, damit man auf nicht über Google bereitgestellte Dienste zugreifen kann.
299
13 Mashups mit öffentlichen Webservices
13.1.2 Zend_Service_Akismet Uns ist dieser Dienst von Akismet bereits bekannt, weil wir in Kapitel 12 damit die Rezensionen der User auf potenziellen Spam gefiltert haben: Automattic (sic!) Kismet (kurz Akismet) ist ein gemeinschaftliches Unterfangen, damit Kommentar- und Trackback-Spam kein Problem mehr darstellt. So können Sie sich endlich wieder aufs Bloggen konzentrieren und brauchen sich nie wieder Sorgen um Spam zu machen. Original unter http://akismet.com/
Der Dienst kam ursprünglich mit dem Blog-System WordPress auf und bietet einen Spamfilter für Leserkommentare, kann aber auch für beliebige anderen Daten eingesetzt werden. Mit Zend_Service_Akismet können Sie außerdem durch den Filter geschlüpften Spam sowie auch falsche Positive an Akismet zurücksenden. Natürlich müssen wir auch demonstrieren, wie man das mit Zend_Service_Akismet macht, nachdem wir in Kapitel 12 das Gleiche schon mit Akismet gemacht haben. Das finden Sie in Listing 13.1. Listing 13.1 Eine Rezension mit Zend_Service_Akismet auf möglichen Spam filtern require_once 'Zend/Service/Akismet.php'; $apiKey = 'f6k3apik3y'; $akismet = new Zend_Service_Akismet( $apiKey, 'http://places/' ); Erforderlich sind
Richtet Verbindung mit API-Schlüssel ein
nur user_ip und user_agent $data = array( 'user_ip' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'comment_type' => 'comment', 'comment_author' => $_POST['author'], 'comment_author_email' => $_POST['email'], 'comment_content' => $_POST['message'] ); if ($akismet->isSpam($data)) { Prüft Daten // Mark as spam to be reviewed by admin auf Spam } else { // Continue }
Wir beginnen, indem wir die Verbindung mit Akismet einrichten und den erforderlichen API-Schlüssel holen. Dann kompilieren wir ein zu prüfendes Daten-Array, zu dem die erforderlichen user_ip und user_agent gehören und auch einige andere Informationen, die wir zur Bestimmung brauchen, um welche Inhalte es sich handelt. Schließlich schicken wir die Daten an Akismet, damit dort geprüft wird, ob es sich um Spam handelt, und dann entsprechende Maßnahmen getroffen werden.
300
13.1 Der Zugriff auf öffentliche Webservices
13.1.3 Zend_Service_Amazon Amazon ist wahrscheinlich der größte und bekannteste Online-Shop im Internet. Was Google für Suchfunktionen ist, ist Amazon für E-Commerce, und die Vision von Amazon ist kaum weniger breit gefasst: Unsere Vision ist, das Unternehmen mit der stärksten Kundenzentrierung weltweit zu sein. Wir wollen einen Ort schaffen, zu dem Menschen kommen und alles finden und entdecken können, was sie online kaufen wollen. Original unter http://phx.corporate-ir.net/phoenix.zhtml?c=97664&p=irol-faq Zend_Service_Amazon gibt Entwicklern die Möglichkeit, über den Webservice der Amazon-API auf Informationen über Artikel zuzugreifen, z. B. Bilder, Preise, Beschreibungen, Rezensionen und damit zusammenhängende Produkte. Wir demonstrieren deren Fähigkeiten später, wenn wir damit eine Buchauswahl suchen, die für die Nutzer der PlacesApplikation nützlich sein können.
13.1.4 Zend_Service_Audioscrobbler Audioscrobbler ist die Engine hinter der Social Music-Site Last.fm, die Ihren musikalischen Geschmack beobachtet und Ihnen neue Musik vorstellt. Diese lernfähige Engine wird wie folgt beschrieben: Das Audioscrobbler-System ist eine gewaltige Datenbank, die Hörgewohnheiten festhält und die anhand der Musik, die man gerne hört, Beziehungen und Empfehlungen berechnet. Original unter http://www.audioscrobbler.net/
Mit der Webservice-API von Audioscrobbler kann man auf Daten über User, Künstler, Alben, Tracks, Tags, Gruppen und Foren zugreifen. Für unsere Places-Applikation nutzen wir diesen Dienst, um Musik zu finden, die Kinder auf den Ausflügen vielleicht gerne hören.
13.1.5 Zend_Service_Delicious Neben der kürzlichen Aktualisierung seines vorher unaussprechlichen Web-2.0.Namens „del.icio.us“ hat auch die Beschreibung dieses im Besitz von Yahoo! befindlichen Dienstes vereinfacht: Delicious ist ein Dienst für das Social Bookmarking, mit dem User Webseiten aus einer zentralisierten Quelle taggen, speichern, verwalten und weitergeben können. Original unter http://delicious.com/about
Über Zend_Service_Delicious kann lesend und schreibend auf Delicious-Posts zugegriffen werden, auf öffentliche Daten nur lesend. Neben den offensichtlichen persönlichen Nutzungsmöglichkeiten für eine solchen Applikations-API können wir damit auf statisti-
301
13 Mashups mit öffentlichen Webservices sche Informationen über User zugreifen, die unsere Places-Website als Bookmark abgelegt haben.
13.1.6 Zend_Service_Flickr Das zu Yahoo! gehörende Flickr hat eine ebenso einfache wie selbstbewusste Beschreibung: Flickr – die wahrscheinlich beste Online-Fotoplattform der Welt. Original unter http://www.flickr.com/about/
In Abschnitt 13.3 werden wir nur lesend mit der Daten-API von Flickr auf Listen von Bildern zugreifen, die zu spezifischen Tags, Benutzerinformationen u. a. passen, um eine Auswahl von Bildern darzustellen, die auf Schlüsselwörter in unseren Places-Artikeln zutreffen.
13.1.7 Zend_Service_Gravatar Gravatar (steht für Globally Recognized Avatar) gehört nun zu Automattic, das auch für den Akismet-Dienst zuständig ist. Das erklärt teilweise die vollmundige Beschreibung für eine aktuelle Möglichkeit, die Identifizierungsicons von Usern (auch als Avatare bezeichnet) zu zentralisieren: Gravatar will Namen Gesichter verleihen. Das ist die Grundlage des Vertrauens. Zukünftig wird Gravatar eine Möglichkeit sein, im Internet Vertrauen zwischen Produzenten und Konsumenten aufzubauen. Original unter http://site.gravatar.com/about
Die Komponente befindet sich aktuell im Inkubator, hat aber zum Ziel, dass man auf die Avatare der Benutzer zugreifen kann, damit die Verfasser von Places-Rezensionen auch ein Gesicht bekommen.
13.1.8 Zend_Service_Nirvanix Nirvanix ermöglicht einen Lese-Schreib-Zugriff auf seinen Online-Speicherdienst: Nirvanix ist der Premium-Anbieter für „Cloud Storage“. Nirvanix hat einen globalen Cluster von Speicherknoten erstellt, allgemein als SDN (Storage Delivery Network) bezeichnet, der vom IMFS (Internet Media File System) von Nirvanix betrieben wird. Original unter http://www.nirvanix.com/company.aspx
Bei Places kann dies exakt wie beschrieben für die Online-Speicherung von Medien genutzt werden, die wir nicht anhand eigener Hosting-Dienste speichern und bereitstellen wollen, z. B. Videos und andere große Dateien.
302
13.1 Der Zugriff auf öffentliche Webservices
13.1.9 Zend_Service_RememberTheMilk Remember The Milk ist – wie der Name schon sagt, die beste Art, Ihre Aufgaben zu verwalten. Vergessen Sie niemals wieder die Milch (oder irgendetwas anderes). http://www.rememberthemilk.com/?hl=de
Die Places-Entwickler könnten dies gemeinsam mit dem in Kapitel 10 zusammengestellten Support-Tracker nutzen, um anfallende Aufgaben nachzuverfolgen. Eine weitere Idee wäre, Eltern dabei zu unterstützen, ihren Ausflug anhand von Places zu organisieren.
13.1.10Zend_Service_Simpy Wie Delicious ist Simpy ein Social Bookmarking-Dienst: Simpy ist ein Dienst für das Social Bookmarking, über den Sie Ihre Lesezeichen, Notizen, Gruppen und mehr speichern, taggen, suchen und weitergeben können. Original unter http://www.simpy.com/
Ob man lieber diesen Dienst oder Delicious nutzt, bleibt weitgehend Geschmackssache, doch die Nutzung ist im Prinzip ähnlich.
13.1.11Zend_Service_SlideShare Bei SlideShare werden Präsentationen als PowerPoint-, Open Office- und PDF-Dateien gehostet. Man kann sie weltweit darstellen oder wie die Firma es ausdrückt: SlideShare ist die beste Art, der ganzen Welt Ihre Präsentationen zu zeigen. Original unter http://www.slideshare.net/
Mit Zend_Service_SlideShare können wir auf SlideShare gehostete Diashows in Places zeigen und auch eigene Präsentationen für potenzielle Places-Investoren oder für die nächste Entwicklerkonferenz Präsentationen unserer großartigen Webapplikation hochladen!
13.1.12Zend_Service_StrikeIron Der StrikeIron-Dienst ist ein wenig schwerer zu beschreiben, weil er eigentlich eine Sammlung kleinerer Dienste ist: Durch die Datendienste von StrikeIron können Sie live auf Daten zum sofortigen Gebrauch zugreifen, in Applikationen integrieren oder in Websites einbauen. Original unter http://www.strikeiron.com/
Drei von Hunderten verfügbarer Dienste (ZIP Code Information, U.S. Address Verification und Sales & Use Tax Basic) haben in Zend_Service_StrikeIron unterstützende Wrap-
303
13 Mashups mit öffentlichen Webservices per, doch die API kann auch mit vielen der anderen Dienste verwendet werden. Die meisten, wenn nicht gar alle Dienste müssen abonniert werden, wobei die Kosten nach der Anzahl der Hits dieses Dienstes berechnet werden.
13.1.13Zend_Service_Technorati User von WordPress kennen das im Dashboard von WordPress integrierte Technorati möglicherweise schon. Es zeigt eingehende Links an, doch die Beschreibung von Technorati selbst lässt größere Ansprüche vermuten: Technorati ist die anerkannte Autorität dafür, was jetzt in diesem Augenblick im World Live Web passiert. Das Live Web ist der dynamische und ständig aktualisierte Bereich des Webs. Wir durchsuchen, entdecken und organisieren Blogs und andere Formen unabhängiger, von Usern generierter Inhalte (Fotos, Videos, Voting etc.), die immer mehr als „Bürgermedien“ bezeichnet werden. Original unter http://technorati.com/about/
Mit Zend_Service_Technorati können wir Blog-Informationen suchen und auslesen, und bei Places werden wir diese Komponente wahrscheinlich ähnlich nutzen wie WordPress: um eingehende Links zu tracken und herauszufinden, was man über uns schreibt. Listing 13.2 zeigt ein Beispiel, wie das gemacht werden kann. Listing 13.2 Auf Places eingehende Links prüfen require_once 'Zend/Service/Technorati.php'; $technorati = new Zend_Service_Technorati('PLACES_API_KEY'); $results = $technorati->cosmos('http://www.placestotakethekids.com/');
Dieses kurze, aber effektive Beispiel holt die Resultate eines Suchlaufs über Blogs ab, die auf die Places-URL verlinken.
13.1.14Zend_Service_Yahoo Yahoo! ist ein weiteres Unternehmen, das wegen seiner Größe und Allgegenwärtigkeit schwer zu definieren ist: Ziel von Yahoo! ist es, für Internetnutzer weltweit Startpunkt in die digitale Welt zu sein und seinen Communitys aus Nutzern, Werbekunden, Publishern und Entwicklern ein unverzichtbares Online-Erlebnis zu bieten, das auf gegenseitigem Vertrauen beruht. http://yahoo.enpress.de/faq.aspx Zend_Service_Yahoo konzentriert sich auf Suchläufe in Yahoo! Web Search, Yahoo! News, Yahoo! Local und Yahoo! Images. Ein praktisches Beispiel für die PlacesApplikation wäre, mit dem Code in Listing 13.3 die Indexierung unserer Site zu prüfen.
304
13.2 Werbeanzeigen mit Amazon-Webservices darstellen Listing 13.3 Die Indexierung der Places-Site prüfen require_once 'Zend/Service/Yahoo.php'; $yahoo = new Zend_Service_Yahoo('PLACES_YAHOO_APPLICATION_ID'); $results = $yahoo->pageDataSearch('http://www.placestotakethekids.com/');
Ihnen ist vielleicht aufgefallen, wie sehr der Code in diesem Yahoo!-Beispiel dem vorigen mit Technorati gleicht. Das ist zwar gewissermaßen zufällig, demonstriert aber gut, wie die Webservice-Komponenten eine große Vielfalt an Diensten mit einem relativ simplen Ansatz vereinfachen und verfügbar machen. Nach dieser kurzen Aufzählung einiger im Zend Framework verfügbaren Webservices wird es Zeit, dass wir uns anhand von Beispielen eingehender mit den Komponenten beschäftigen. Wir beginnen, indem wir einige Artikel von Amazon über die Webservice-API von Amazon auslesen.
13.2 Werbeanzeigen mit Amazon-Webservices darstellen Ihnen ist in den vorigen Kapiteln wahrscheinlich aufgefallen, dass es rechts neben der Places-Applikation recht viel Platz gibt, der regelrecht auf Anzeigen wartet. Nachdem wir die verfügbaren Webservices durchgegangen sind, ist offensichtlich, dass wir in Zend_Service_Amazon einen Kandidaten dafür haben. Weil sich dieser Leerraum in der Layoutdatei befindet, werden wir eine View-Hilfsklasse verwenden, dessen Zweck es wie im Manual angegeben ist, „bestimmte komplexe Funktionen immer wieder zu wiederholen“. Das passt sehr gut zu unserem Anliegen, die gleichen View-Daten wiederholt von außerhalb eines Controllers zu zeigen. Das Auslesen von Daten ist allerdings ein Job für eine Model-Klasse, und darum kümmern wir uns als Erstes.
13.2.1 Die Amazon-Model-Klasse Vor der Abfrage von Amazon müssen wir erst ein paar Einstellungen vornehmen. Die Model-Klasse, die wir einrichten, ist speziell auf die Anforderungen unserer PlacesApplikation zugeschnitten, könnte aber bei Bedarf auch allgemeiner gehalten werden. Bei unseren Beispielen ist uns daran gelegen, die Aufteilung der Verantwortlichkeiten zu demonstrieren, wobei die Model-Klasse sich um das Auslesen der Daten kümmert, die dann von der View-Hilfsklasse verwendet werden. Unseres Erachtens brauchen wir nur eine kleine Auswahl von Büchern, um den Werbebereich zu füllen. Diese Auswahl soll zu einigen Schlüsselwörtern passen, die in der Layoutdatei der View-Hilfsklasse übergeben werden. Ein Vorschaubild des Umschlags und ein Titel, der mit der übergeordneten Seite von Amazon verlinkt ist – mehr brauchen wir für diese Bücher nicht. Nachdem wir den erforderlichen API-Schlüssel von Amazon angegeben haben, verwendet der Code in Listing 13.4 das Fluent-Interface von Zend_Service_Amazon_Query, um einige unserer Anforderungen zu übergeben. Wie Sie an einen API-Schlüssel und anderes kom-
305
13 Mashups mit öffentlichen Webservices men, erfahren Sie auf der Website über die Amazon-Webservices unter http://aws.amazon. com/. (Auf den deutschsprachigen Seiten von Amazon.de werden die Webservices bisher nicht angeboten.) Listing 13.4 Die zum Auslesen der Daten verwendete Amazon-Model-Klasse require_once 'Zend/Service/Amazon/Query.php'; class Amazon { protected $apiKey; public function __construct() { $this->apiKey = Zend_Registry::get('config') ->amazon_api_key; } public function search($keywords) { if(empty($this->apiKey)) { return null; } Fasst Abfrage in
Liest Amazon-
API-Schlüssel aus
Stoppt, falls kein
API-Schlüssel verfügbar ist Instanziiert Zend_Service_ Amazon_Query
einen try-catch-Block ein try { $query = new Zend_Service_Amazon_Query( $this->apiKey, 'UK' Sucht nur Übergibt zu ); nach Büchern suchende $query->category('Books') ->Keywords($keywords) Schlüsselwörter ->ResponseGroup('Small,Images'); $results = $query->search(); Gibt die if ($results->totalResults() > 0) { Antwortgruppe an return $results; } } catch (Zend_Service_Exception $e) { return null; } Gibt null zurück, return null;
}
falls kein Dienst vorhanden
}
Der API-Schlüssel wird in einem Zend_Config-Objekt gespeichert, das wiederum in einem Zend_Registry-Objekt abgelegt ist und im Konstruktor unserer Klasse ausgelesen wird n. Falls kein API-Schlüssel vorhanden ist, bricht die Suchmethode sofort ab und gibt einen null-Wert zurück o. Weil wir keine Kontrolle über die Verfügbarkeit von öffentlichen Webservices oder über die Netzwerkverbindungen haben, muss unser Code sich darum kümmern. Bei solchen Gelegenheiten ist das neue Exception-Model von PHP5 besonders wertvoll. In diesem Fall haben wir die Abfrage in einen try-catch-Block gesetzt und erlaubt, dass er stillschweigend fehlschlagen kann, weil dessen Verfügbarkeit für den Betrieb unserer Website nicht wesentlich ist p. Wenn alles okay ist, richten wir zuerst das Zend_Service_Amazon_Query-Objekt mit dem API-Schlüssel ein und geben Amazon UK als Dienst an q. Dann richten wir die Optionen
306
13.2 Werbeanzeigen mit Amazon-Webservices darstellen für die Suche ein und beginnen mit der Festlegung, dass nur nach Büchern gesucht werden soll r. Als Nächstes setzen wir die Schlüsselwörter s und die Antwortgruppe t, die „die von der Operation zurückgegebenen Daten steuert“. In diesem Fall haben wir die Gruppe Small angegeben, die „globale Daten auf Artikelebene enthält (keine Preisangaben oder Verfügbarkeit), z. B. die Standardidentifikationsnummer von Amazon (ASIN, Produkttitel, Urheber, Autor, Künstler, Komponist, Verzeichnis, Hersteller etc.), Produktgruppe, URL und Hersteller“, und die Gruppe Images, damit wir ein paar Vorschaubilder zum Zeigen haben. Wenn etwas nicht in Ordnung ist, geben wir einen null-Wert zurück u, doch wir könnten auch noch etwas Abenteuerliches machen, wenn uns danach ist. Da unsere Model-Klasse nun Abfrageanforderungen akzeptiert, können wir uns der ViewHilfsklasse zuwenden, die die View-Daten aufbereitet.
13.2.2 Die View-Hilfsklasse amazonAds Unsere View-Hilfsklasse akzeptiert die von der Amazon-Model-Klasse in Listing 13.4 ausgelesenen Daten und formatiert sie zu einer unsortierten HTML-Liste. Diese kann dann in die Layout-Datei der Site wie folgt eingebunden werden: amazonAds('kids,travel', 2); ?>
Damit geben wir an, dass die zu suchenden Schlüsselwörter „kids, travel“ lauten und dass 3 Bücher angezeigt werden sollen (wenn also die foreach-Schleife auf 2 springt). In Listing 13.5 sehen wir, dass die amazonAds()-Methode im Code der View-Hilfsklasse auch einen dritten Parameter annimmt, wobei es sich um eine optionale UL-Element-ID handelt. Wie bei allen View-Hilfsklassen ist die Bezeichnung dieser Methode wichtig und muss zum Klassennamen passen (abgesehen vom kleingeschriebenen ersten Zeichen), damit sie automatisch aufgerufen werden kann, wenn das Objekt in View-Dateien verwendet wird. Listing 13.5 Die View-Hilfsklasse AmazonAds stellt das HTML für die View zusammen. require_once 'Amazon.php'; class Zend_View_Helper_AmazonAds { public function amazonAds($keywords, $amount=3, $elementId='amazonads') Instanziiert das { Führt Abfrage anhand Amazon-Model-Objekt $amazon = new Amazon; der Schlüsselwörter durch $results = $amazon->search($keywords); if(null === $results) { return null; Richtet zurückGibt null zurück, }
Die Methode beginnt mit der Instanziierung des Amazon-Model-Objekts aus Listing 13.4. Dann verwenden wir es für die Abfrage mit den angeforderten Schlüsselwörtern. Wenn die Suche keine Treffer liefert, geben wir einen null-Wert zurück; anderenfalls bauen wir das HTML zusammen, das in der View-Datei zurückgegeben und dargestellt wird. Weil Sie dieses Buch lesen, sind Sie offensichtlich pfiffig genug, um auch ohne Ausprobieren dieses Codes zu merken, dass er ein paar bemerkenswerte Fehler aufweist. Das erste Problem ist, dass er einen null-Wert zurückgibt, wenn er fehlschlägt, was zu einem leeren Raum auf unserer Seite führt. Das zweite Problem ist, dass er bei Erfolg möglicherweise immer noch eine merkliche Verzögerung verursacht, weil die Information bei Amazon jedes Mal ausgelesen wird, sobald eine Seite auf unserer Site angefordert wird. Das erste Problem ist nicht so schwer zu beheben. Wir könnten beispielsweise einfach eine andere View-Hilfsklasse aufrufen, die eine Bannerwerbung zeigt, falls die AmazonAbfrage fehlschlägt. Das zweite Problem ist nicht nur für uns nicht steuerbar, sondern auch relevanter, weil User durch lästige Verzögerungen im Seitenaufbau von unserer Seite vertrieben werden könnten. Wir brauchen also einen Weg, um die Anfälligkeit für Netzwerkverzögerungen zu reduzieren. Dazu cachen wir anhand der Zend_Cache-Komponente das Ergebnis der View-Hilfsklasse.
13.2.3 Die View-Hilfsklasse cachen In diesem Abschnitt stellen wir das Caching vor, um die Zahl der Abfrageaufrufe zu reduzieren, die bei Amazon gemacht werden müssen. Doch ist das nicht als detaillierte Einführung ins Caching gedacht, weil dies im nächsten Kapitel Thema ist. Nichtsdestotrotz nehmen wir uns etwas Zeit, um das in Listing 13.6 gezeigte Beispiel zu erläutern. Listing 13.6 Die View-Hilfsklasse AmazonAds mit integriertem Caching über Zend_Cache require_once 'Amazon.php'; class Zend_View_Helper_AmazonAds { protected $cache; protected $cacheDir;
308
13.2 Werbeanzeigen mit Amazon-Webservices darstellen public function __construct() { $this->cacheDir = ROOT_DIR . '/application/cache'; $frontendOptions = array( 'lifetime' => 7200, 'automatic_serialization' => true );
Setzt CacheSpeicherverzeichnis Setzt Lebensdauer von 2 Stunden
Die gecachete Version der View-Hilfsklasse wird eine Kopie des HTML-Outputs in einer Datei speichern, die so eingestellt ist, dass sie alle zwei Stunden aktualisiert wird. Jedem Aufruf einer View-Hilfsklasse geht eine Prüfung voran, ob es eine aktuelle Cache-Datei gibt, die als Output der View-Hilfsklasse verwendet werden kann. Wenn keine aktuelle Cache-Datei verfügbar ist, machen wir eine Anfrage bei Amazon und speichern vor dem
309
13 Mashups mit öffentlichen Webservices Ausgeben das HTML in der Cache-Datei, damit es für die nächste Anfrage verwendet werden kann. Der Vorteil von alledem ist, dass wir höchstens alle zwei Stunden eine Anfrage bei Amazon machen müssen anstatt bei jeder Seitenanfrage. Weil wir bloß eine Auswahl von Büchern zeigen, die unseres Erachtens für unsere Leser interessant sein könnten, ist die zweistündige Verzögerung zwischen der Datenaktualisierung unwesentlich. Falls wir aktuellere Daten bräuchten, müssten wir nur die Lebensdauer in den Frontend-Optionen verringern. In Abbildung 13.1 sehen Sie, dass der vorher leere Raum nun mit einer Auswahl von Büchern gefüllt ist, die mit Amazon verlinkt sind. So können wir außerdem über das Affiliate-Programm von Amazon mit der Site prozentual an Buchverkäufen verdienen. Das wird gecachet, um unnötige Verzögerungen beim Laden der Seite zu vermeiden, aber perfekt ist das nicht, weil die Bilder selbst immer noch von Amazon ausgelesen werden müssen. Für unseren Bedarf ist das akzeptabel. Mit dem HTML kann die Seite gerendert werden, und die User akzeptieren allgemein eine gewisse Verzögerung beim Laden von Bildern. Wenn die Nachfrage hoch genug wäre, könnten wir die Bilder auch cachen, aber wir müssten dazu vorher noch einmal die Nutzungsbedingungen der Amazon-Webservices prüfen.
Abbildung 13.1 Die neue Amazon-Werbung wird rechts gezeigt und eine ähnliche Suche bei Amazon links.
Ein unglücklicher Nebeneffekt durch das Einfügen der Amazon-Vorschaubilder ist, dass die Inhalte der Site, vor allem die Artikel, nun recht kärglich aussehen. Im nächsten Abschnitt werden wir das beheben, indem die Artikel durch Bilder von Flickr ergänzt werden.
310
13.3 Darstellen von Flickr-Bildern
13.3 Darstellen von Flickr-Bildern Bevor wir uns ans nächste Beispiel machen, sollte noch einmal betont werden, dass das Folgende nur zu Demonstrationszwecken gedacht ist. Für den tatsächlichen Einsatz müssten wir sorgfältig darauf achten, dass eine Verwendung der Bilder auch den Nutzungsbedingungen von Flickr entspricht, wozu Lizenzbeschränkungen bei einzelnen Bildern genauso gehören wie Einschränkungen dessen, was von Flickr als kommerzielle Nutzung betrachtet wird. Wir wollen in unseren Artikeln relevante Bilder aus Flickr darstellen, doch die Intention dieses Beispiels hier ist die Nutzung eines öffentlichen Webservices in einem ActionController und weniger, wie man den Service von Flickr auf eine bestimmte Art und Weise nutzen kann. Nach dieser Anmerkung machen wir uns an das Beispiel. Die bei Zend_Service_Flickr verfügbaren Methoden sind nur ein relativ kleiner Ausschnitt der über die Flickr-API bereitgestellten Methoden und im Grunde darauf beschränkt, Bilder anhand von Tags, Userinformationen oder gewissen Bilddetails zu finden. Das könnte zwar auch verwendet werden, um die Bilder eines bestimmten Users bei Flickr auszulesen, damit er in einem von eben dieser Person übermittelten Artikel gezeigt wird, aber wir arbeiten in diesem Beispiel mit der allgemeineren Tag-Suche. Der Ausgangspunkt ist der gleiche wie bei dem Amazon-Beispiel: eine fürs Auslesen der Flickr-Daten verantwortliche Model-Klasse.
13.3.1 Die Flickr-Model-Klasse Die Model-Klasse, die hier im Zusammenhang mit Flickr vorgestellt wird, ähnelt derjenigen sehr, die wir für das Amazon-Beispiel entwickelt haben. Wie Amazon braucht auch Flickr einen API-Schlüssel, den Sie über die API-Dokumentationsseite unter http://www.flickr.com/services/api/ bekommen (damit wird die Nutzung der API getrackt). Listing 13.7 zeigt, dass wir wieder ein fürs Auslesen von Daten verantwortliches Model entwickeln, doch dieses Mal wird es im Action-Controller für den Artikel verwendet. Listing 13.7 Die Flickr-Model-Klasse require_once 'Zend/Service/Flickr.php'; class Flickr { protected $apiKey; public function __construct() { $this->apiKey = Zend_Registry::get('config') ->flickr_api_key; Holt API-Schlüssel von }
Zend_Config-Objekt
public function search($keywords, $amount=6) { {
311
13 Mashups mit öffentlichen Webservices { $this->apiKey = Zend_Registry::get('config') ->flickr_api_key;
Holt API-Schlüssel von Zend_Config-Objekt
}
public function search($keywords, $amount=6) { if(empty($this->apiKey)) { Abbruch, falls kein return null; API-Schlüssel vorhanden } try { $flickr = new Zend_Service_Flickr( $this->apiKey); $results = $flickr->tagSearch($keywords, array( 'per_page' => $amount, 'tag_mode' => 'all', 'license' => 3 ));
Verwendet try-catch-Block zur Fehlerbehandlung
Instanziiert Zend_Service_Flickr mit API-Schlüssel Sucht anhand von Schlüsselwörtern und Optionen
if ($results->totalResults() > 0) { return $results; } } catch (Zend_Service_Exception $e) { return null; Gibt bei Fehlschlagen } null zurück return null; } }
In Listing 13.7 wird der API-Schlüssel aus dem config-Objekt wiederhergestellt und an das Zend_Service_Flickr-Objekt übergeben. Als Nächstes übergeben wir der tagSearch()-Methode einige Optionseinstellungen. Sie ist ein Wrapper für die FlickrMethode flickr.photos.search. Es lohnt, sich einmal die API-Dokumentation anzuschauen, weil die Liste der verfügbaren Optionen riesig ist und eine große Variation in der Art der zurückgegebenen Ergebnisse erlaubt. Wir könnten unsere Suche auf unterschiedliche Datumseinstellungen, einen bestimmten User, einen geographischen Bereich, eine Gruppe oder etwas anderes eingrenzen. In unserem Fall geben wir ein Limit pro Seite an, dass beim Suchlauf alle von uns angegebenen Tags berücksichtigt werden und dass die Lizenz der zurückgegebenen Bilder zu unserem Einsatzzweck passt. In diesem Fall zeigt der Wert 3 an, dass die Creative Commons-Lizenz No Derivative Works (Keine abgeleiteten Werke) verwendet wird, die wie folgt definiert wird: Keine abgeleiteten Werke. Dieses Werk darf nicht bearbeitet oder in anderer Weise verändert werden. http://creativecommons.org/licenses/by-nc-nd/3.0/de/
Wenn man die Resultate auf Gruppen beschränken könnte, wäre das besonders hilfreich, weil wir dann mit Gruppen von Bildern arbeiten könnten, über die wir eine gewisse Kontrolle haben, doch im Rahmen dieses Beispiels halten wir die Sache schlicht und einfach.
312
13.3 Darstellen von Flickr-Bildern Mit dieser Anmerkung ist unsere Beispiel-Model-Klasse nun vollständig genug, damit wir mit ihrer Integration in den Action-Controller weitermachen können.
13.3.2 Flickr in einem Action-Controller verwenden Beim Amazon-Beispiel arbeiteten wir mit einer View-Hilfsklasse, weil wir sie wiederholt auf verschiedenen Seiten verwenden wollten und sie nicht speziell für eine bestimmte Seite gedacht war. In diesem Fall werden wir die Flickr-Bilder nur mit Artikeln zusammen einsetzen; also reicht es völlig aus, das Model aus der Artikel-Controller-Action aufzurufen. Weil wir eine Suche basierend auf den Tags von Bildern ausführen, müssen wir eine Möglichkeit finden, an Suchworte zu kommen. Das machen wir, indem wir eine Schlüsselwortspalte in die Artikeltabelle der Places-Datenbank einfügen und (wie in Listing 13.8 gezeigt) diese Schlüsselwörter an das Flickr-Objekt aus dem vorigen Abschnitt übergeben. Listing 13.8 Die Flickr-Model-Klasse in der Controller-Action für die Artikel einsetzen public function indexAction() { $id = (int)$this->_request->getParam('id'); if ($id == 0) { $this->_redirect('/'); return; } $articlesTable = new ArticleTable(); $article = $articlesTable->fetchRow('id='.$id); if ($article->id != $id) { $this->_redirect('/'); return; } $this->view->article = $article; include_once 'Flickr.php'; Richtet Flickr$flickr = new Flickr; Objekt ein $results = $flickr->search($article->keywords); $this->view->flickr = $results; }
Holt den Artikel
Suche über ArtikelSchlüsselwörter
Übergibt Treffer an View
In Listing 13.8 sehen Sie den originalen Code zum Auslesen eines angeforderten Artikels in der ArticleController::indexAction()-Methode und darunter unseren Code für die entsprechenden Bilder aus Flickr. Nach dem Auslesen des Artikels (basierend auf der an den Action-Controller übergebenen ID) übergeben wir die Schlüsselwörter dieses Artikels dann an die Flickr-Klasse. Schließlich übergeben wir die Resultate an die View (siehe Listing 13.9).
313
13 Mashups mit öffentlichen Webservices Listing 13.9 Die Artikel-View mit den eingefügten Flickr-Bildern
escape($this->article->title); ?>
article->body; ?>
Reviews
reviews)) : ?>
reviews as $review) : ?>
escape($review->user_name); ?> on displayDate($review->date_updated); ?>
escape($review->body); ?>
Prüft, ob Ergebnisse flickr->totalResults() > 0): ?>
Der zusätzliche Code im View ist recht unkompliziert. Er besteht aus einem einfachen Test, damit wir sicher sind, auch Ergebnisse für unsere Schleife zu haben. Dann werden ein verlinktes Bildelement und ein Bildtitel in einer unsortierten Liste eingerichtet. Wenn wir dann noch etwas mit CSS zaubern, können wir diese Bilder floaten lassen und einen Galerie-Effekt erzielen (siehe Abbildung 13.2). Sie sehen die Originalbilder aus der TagSuche in Flickr, die nun in unserem Artikel über den Edinburgher Zoo erscheinen.
314
13.4 Mit Zend_Gdata auf Google zugreifen
Abbildung 13.2 Rechts werden die Flickr-Bilder, die auf Schlüsselwörtern aus den Artikeln basieren, mit dem Artikel dargestellt und links auf der Flickr-Site die Originalbilder.
Vielleicht haben Sie bemerkt, dass wir in diesem Beispiel nicht mit Caching arbeiten. Das lag teilweise daran, dass wir den Schwerpunkt auf die Webservice-Komponente gelegt haben, aber auch daran, dass das Vorhandensein des HTML (wie im Amazon-Abschnitt erwähnt) bedeutet, dass der Aufbau der ganzen Seite nicht über Gebühr durch Netzwerkverzögerungen belastet wird. In der Praxis ist es wahrscheinlich eine gute Idee, sich ums Caching zu kümmern. Hier müssen aber die Nutzungsbedingungen der Flickr-API berücksichtigt werden, die davor warnen, „Fotos von Flickr-Usern länger als einen angemessenen Zeitraum zu cachen oder zu speichern, um Flickr-Usern Ihren Service anbieten zu können“. Nachdem wir gesehen haben, wie sehr die Amazon-Werbung die leere rechte Seite der Site verbessert und wie die Flickr-Bilder unsere Artikel aufwerten, sind wir bereit für eine größere und visuell noch dynamischere Herausforderung: das Einfügen von Videos. Im nächsten Abschnitt machen wir uns daran und nutzen die zweifellos größte WebserviceKomponente von Zend Framework: Zend_Gdata.
13.4 Mit Zend_Gdata auf Google zugreifen Als wir weiter vorne Gdata vorgestellt haben, gaben wir eine kurze Übersicht der verschiedenen, über die Google Data-API verfügbaren Dienste. Unsere Site Places to take the kids! würde eindeutig von den Videos profitieren, die ein weiterer dieser Dienste bereitstellt: die YouTube-API.
315
13 Mashups mit öffentlichen Webservices Videos auf einer Site einzubauen, ist kein leichtes Unterfangen. Das erfordert nicht nur eine durchdachte Vorbereitung des Videos für die Bereitstellung im Internet, sondern belastet womöglich überdies auch Hosting-Space und Bandbreite. Der Erfolg von YouTube und insbesondere der Möglichkeit, die dortigen Videos einzubetten, spiegelt eindeutig die Vorteile wider, bei Ihren Videos mit Outsourcing zu arbeiten. Natürlich wird jedes Video, das Sie aus YouTube einbinden und verwenden, das Wasserzeichen von YouTube tragen, wodurch es für bestimmte Anforderungen unpassend wird. In solchen Fällen könnten Lösungen wie Zend_Service_Nirvanix eine mögliche Option sein, um den Anforderungen an Hosting und Bandbreite gerecht zu werden. Wir machen uns keine Gedanken über die YouTube-Verbindung zu Places, sondern könnten es sogar als Möglichkeit nutzen, Traffic auf unsere Site zu ziehen. Wie bei allen öffentlichen Diensten muss zuerst einmal in Erfahrung gebracht werden, was man den Nutzungsbedingungen entsprechend machen darf. Unsere Absicht ist, bei Places einen Videobereich einzubauen, der zuerst eine Liste der Videokategorien zeigt, dann eine Liste der Videos in einer gewählten Kategorie und schließlich ein ausgewähltes Video. Das alles entspricht offenbar den Nutzungsbedingungen von YouTube. Nach Erledigen dieser Prüfung fangen wir damit an, die Seite mit den Videokategorien zu erstellen. Wie beim Flickr-Beispiel in Abschnitt 13.3 nehmen wir dafür einen Action-Controller.
13.4.1 Die YouTube-API in einem Action-Controller Das Interessante an dem Beispiel dieses Abschnitts ist, wie wenig Coding erforderlich ist, um die Places-Site beträchtlich aufzuwerten. Wir werden gleich jede Seite des Videobereichs im Detail anschauen, doch halten wir einmal kurz inne und schauen uns an, wie kurz und knapp die Controller-Action in Listing 13.10 ist. Immerhin sollten wir es nicht nur den Ruby on Rails-Entwicklern überlassen, mit der Knappheit ihres Codes anzugeben! Listing 13.10 Die Controller-Action für die Videos include_once 'Zend/Gdata/YouTube.php'; class VideosController extends Zend_Controller_Action { protected $_youTube; function init() { $this->_youTube = new Zend_Gdata_YouTube;
Richtet Zend_ Gdata_YouTubeObjekt ein
}
Holt public function indexAction() Abspiellisten { $this->view->playlistListFeed = $this->_youTube->getPlaylistListFeed('ZFinAction'); } Holt public function listAction() {
316
Abspiellisten-Videos
13.4 Mit Zend_Gdata auf Google zugreifen { $playlistId = $this->_request->getParam('id'); $query = $this->_youTube->newVideoQuery( 'http://gdata.youtube.com/feeds/playlists/' . $playlistId); $this->view->videoFeed = $this->_youTube->getVideoFeed($query); }
Holt
Video public function viewAction() { $videoId = $this->_request->getParam('id'); $this->view->videoId = $videoId; $this->view->videoEntry = $this->_youTube->getVideoEntry($videoId); } }
Aus Gründen der Vollständigkeit sollte angemerkt werden, dass in den View-Dateien eine gewisse Logik passiert. Bevor wir also zu flüchtig werden, sollten wir diese einmal durchsehen, und beginnen dafür mit der Seite mit den Videokategorien. Bitte beachten Sie, dass wir keinen API-Schlüssel für die YouTube-API brauchen, weil wir nur Lesezugriff haben, aber wir haben ein YouTube-Benutzerkonto eingerichtet, mit dem wir arbeiten können.
13.4.2 Die Seite für die Videokategorien Dies ist eine Auswahlseite: Die User können hier auf eine Videokategorie klicken, um die darin enthaltenen Videos zu sehen. Die Controller-Action-Datei in Listing 13.10 hat eine indexAction()-Methode, die für diese Seite verantwortlich ist. Sie holt einfach einen Abspiellisten-Feed für das Benutzerkonto „ZFinAction“ und übergibt ihn an die View: public function indexAction() { $this->view->playlistListFeed = $this->_youTube->getPlaylistListFeed('ZFinAction'); }
YouTube definiert Abspiellisten (playlists) als „Sammlungen von Videos, die man auf YouTube anschauen, an andere weitergeben oder in Websites oder Blogs einbetten kann“. In Abbildung 13.3 sehen Sie die Seite mit dem YouTube-Benutzerkonto, über das wir die Abspiellisten eingerichtet haben. Diese erscheinen dann in der in Listing 13.11 gezeigten View-Datei als unsere Videokategorien. Beachten Sie, dass wir jeder auch eine Beschreibung mitgeben können, die wir auf der Seite ausgeben.
317
13 Mashups mit öffentlichen Webservices
Abbildung 13.3 Rechts die Seite mit den Videokategorien und links die Seite zur Verwaltung der YouTube-Playlisten
Listing 13.11 Der View-Code für die Videokategorien
Videos
Schleife durch Playlist-Feed
playlistListFeed as $playlistEntry): ?> getPlaylistVideoFeedUrl(), '/' Holt ID aus URL ), 1); ?> des Playlisteneintrags
Die View-Datei geht einfach mit einer Schleife den Abspiellisten-Feed durch, filtert die Feed-ID des Abspiellisteneintrags aus dessen URL und verwendet diese ID und die Beschreibung für einen Link auf unsere nächste Seite: die mit den Videolisten.
13.4.3 Die Seite mit den Videolisten Die Videolistenseite zeigt die vom User in der Abspielliste ausgewählten Videos. Abbildung 13.4 zeigt das YouTube-Konto, mit dem wir in jede Abspielliste die Videos eingefügt haben, und wie sie auf der Videolistenseite erscheinen.
318
13.4 Mit Zend_Gdata auf Google zugreifen
Abbildung 13.4 Rechts die Seite mit den Videolisten und links die Seite zur Verwaltung der YouTubeVideolisten
Mit einem Blick auf die Action-Controller-Methode listAction() in Listing 13.10 sehen wir, dass sie die bereits aus dem URL des Abspiellisteneintrags herausgefilterte ID nimmt und sie in einer Abfrage verwendet, die den Video-Feed für die gewählte Playliste holt: public function listAction() { $playlistId = $this->_request->getParam('id'); $query = $this->_youTube->newVideoQuery( 'http://gdata.youtube.com/feeds/playlists/' . $playlistId); $this->view->videoFeed = $this->_youTube->getVideoFeed($query); }
Dieser Video-Feed wird dann in Listing 13.12 an die View-Datei übergeben. Listing 13.12 Die View-Datei für die Videoliste
Schleife
Videos in videoFeed->title->text; ?>
durch
die Videos videoFeed as $videoEntry): ?> getFlashPlayerUrl(), '/'), aus URL heraus 1); ?>
13 Mashups mit öffentlichen Webservices Diese View-Datei ist weitgehend die gleiche wie bei der Seite mit den Videokategorien. Sie führt eine Schleife durch den Video-Feed durch, filtert die Video-ID aus dessen URL und verwendet diese ID dann zusammen mit einer Beschreibung in einem Link auf unsere nächste Seite: die Video-Seite. Beachten Sie, dass noch weitere Optionen verfügbar sind, z. B. Vorschaubilder (Thumbnails), die Sie auch darstellen könnten. Mehr über diese Optionen erfahren Sie unter http://www.youtube.com/dev.
13.4.4 Die Video-Seite Die Video-Seite ist von allen die einfachste, weil sie bloß das Video zu zeigen hat. In Abbildung 13.5 sehen Sie das bei YouTube ausgewählte Video, das nun auf unserer VideoSeite gezeigt wird.
Abbildung 13.5 Rechts die Video-Seite und links das Original-Video auf YouTube
Der Controller-Action-Code in der Methode viewAction(), die sich darum kümmert, ist sehr einfach. Sie arbeitet mit der ihr übergebenen Video-ID, um das Video von YouTube zu holen, und richtet das als View-Variable ein. public function viewAction() { $videoId = $this->_request->getParam('id'); $this->view->videoId = $videoId; $this->view->videoEntry = $this->_youTube->getVideoEntry($videoId); }
Beachten Sie, dass wir diese Video-ID als eigene View-Variable übergeben, weil diese in der View verwendet wird (siehe Listing 13.13), um die erforderlichen URLs zu konstruieren, mit denen die Daten aus YouTube ausgelesen werden.
320
13.5 Zusammenfassung Listing 13.13 Die View-Datei für die Videos
videoEntry->mediaGroup->title->text;
?>
Nutzt Video-ID, um Videodaten auszulesen
In unserer View-Datei in Listing 13.13 werden die videoEntry-Daten verwendet, um den Titel und die ID des Videos zu holen, die im URL enthalten sind, und so kann das Video auf der fertigen Seite erscheinen. Das Endergebnis dessen, was eigentlich recht wenig Arbeit war, ist, dass Sie Ihre Site mit einem komplett über Ihr YouTube-Konto gesteuerten Videobereich beträchtlich aufwerten können. Das bedeutet, Sie können nicht nur öffentliche Videos nutzen, sondern auch eigene hochladen und vorstellen. Jeder, der schon einige Zeit mit dem Internet arbeitet oder sich immer gewünscht hat, seinen eigenen Fernsehsender zu haben, wird erkennen, dass damit sehr spannende Dinge möglich werden.
13.5 Zusammenfassung Im vorigen Kapitel haben wir uns sehr viel mit der Theorie und Praxis der Arbeit mit Webservices anhand von Zend Framework beschäftigt, und zwar client- und auch serverseitig. Dieses Kapitel sollte sich deutlich einfacher angefühlt haben – nicht nur, weil es nur an der Client-Seite gearbeitet hat, sondern auch, weil die Zend_Service_*- und Zend_GdataKomponenten vieles von der Komplexität dieser Dienste in leicht fassbarer Form abdecken. Das hat man schon daran gemerkt, dass wir kaum etwas über die diesen Diensten zugrunde liegenden Technologien sagen mussten. Sie sollten nun eine gute Vorstellung von einigen der verfügbaren WebserviceKomponenten des Zend Frameworks haben und wissen, dass sich noch weitere in der Entwicklung befinden. Besser noch wäre, wenn es Sie inspiriert hat, eigene zu entwickeln! Durch unsere kurzen Beispiele sollten Sie eine Vorstellung davon bekommen, wie man mit diesen Komponenten in eigenen Projekten arbeiten kann. Mit den detaillierteren Beispielen haben Sie eine Vorstellung bekommen, wie diese Komponenten in der MVC-Struktur des Zend Frameworks arbeiten. In einem Kapitel, das so viele Komponenten vorzustellen hatte, gab es natürlich auch Grenzen dessen, wie sehr wir ins Detail gehen konnten. Nichtsdestotrotz hoffen wir, die wichtigsten Punkte angesprochen zu haben, z. B. die Nutzungsbedingungen der Dienste, wie man defensiv programmiert, vor allem hinsichtlich des
321
13 Mashups mit öffentlichen Webservices sehr realen Problems der Netzwerkverzögerung, und wie man die Zahl der erforderlichen Dienstanfragen durch Caching reduziert. Caching ist ein Thema, das sein eigenes Kapitel erfordert, und das kommt nun als Nächstes.
322
14 14 Das Caching beschleunigen Die Themen dieses Kapitels
Die Funktionsweise des Cachings Die Komponente Zend_Cache Die Arbeit mit den Frontend-Klassen von Zend_Cache Was und wie lange gecachet werden soll Beim Caching geht es darum, dass das Ergebnis einer sehr rechenintensiven Aufgabe wie einer Datenbankabfrage oder einer aufwendigen mathematischen Kalkulation gespeichert wird, damit es das nächste Mal schnell ausgelesen werden kann, anstatt die Aufgabe erneut auszuführen. Dieses Kapitel erklärt die Vorteile des Cachings, zeigt die Verwendung von Zend_Cache und erläutert den Prozess, wie man die passenden Cache-Einstellungen bei einer Applikation setzt. Bei einem Projekt von Steven erfuhr sein Kunde, dass der Shared-Hosting-Account, mit dem gearbeitet wurde, zu viele Ressourcen verbraucht, und dass das Konto aufgelöst werde, wenn das Problem nicht behoben werde. Die Site versorgte über 40.000 Mitglieder, von denen viele täglich zu dieser Website kamen. Die Kündigung des Kontos wäre also eine Katastrophe gewesen. Die Site war auch schon schleppend langsam geworden, und immer mehr Datenbank- und Speicherfehler tauchten auf. Bei der Analyse der Site entdeckte Steven, dass die Lastprobleme und die Langsamkeit von der Datenbank verursacht wurden. Er forschte nach und fand heraus, dass eine Abfrage das Problem verursachte: Ein bestimmter Codeabschnitt addierte ein Feld über alle Zeilen in einer Tabelle und gab das Ergebnis zurück. Zuerst lief die Aufgabe gut, doch mit Zunahme der Zeilen (zuerst einige Tausende, dann einige Zehntausende) wurde die Abfrage immer langsamer. Dieser Code wurde auf jeder Seite aufgerufen, und so war offensichtlich, wie das Ladeproblem zustande gekommen war. Angenommen, dass bei täglich 1.000 Besuchern jeder 10 Seiten aufruft, dann wird die Abfrage jeden Tag 10.000 Mal vorgenommen. Weil die meisten Besucher etwa zur glei-
323
14 Das Caching beschleunigen chen Zeit morgens auf die Site kamen, geschah der Großteil der 10.000 Abfragen in einem kurzen Zeitraum, etwa innerhalb von ein bis zwei Stunden. Kein Wunder, dass die Site in die Knie ging! Die Lösung war einfach: Das Ergebnis der Abfrage wurde im Cache gespeichert, der stündlich aktualisiert wurde. Die Abfrage wurde nun nur noch einmal pro Stunde anstatt mehrere Tausend Mal pro Stunde durchgeführt. Sofort sank die Serverlast beinahe auf Null, und die Performance der Site war so gut wie nie zuvor. Letzten Endes wurde durch das Caching ein Upgrade des Hosting-Accounts (oder auch dessen Auflösung) vermieden.
14.1
Die Vorteile des Cachings Vorteilhaft ist Caching vor allem durch die Reduzierung der Ressourcennutzung und die schnellere Bereitstellung der Inhalte. Dass weniger Ressourcen verbraucht werden, bedeutet, dass man mehr User mit einem Account versorgen kann, der weniger kostet. Wenn die Inhalte außerdem schneller verfügbar sind, führt das zu einer besseren User Experience. Das Caching einer Datenbankabfrage bedeutet, dass Ihre Applikation weniger Zeit für die Verbindung mit der Datenbank benötigt, was dazu führt, dass für nicht cachingfähige Datenbankoperationen mehr Ressourcen bereitstehen. Das reduziert auch die Dauer des Seitenaufbaus, was Ressourcen des Servers freisetzt, damit er mehr Seiten liefern kann. Durchs Caching werden verschiedene großartige Dinge möglich: Wo normalerweise eine Steigerung des Traffics bedeutet, dass ladeintensive Aufgaben wie Datenbankabfragen zunehmen, wird durch Einsatz von Caching nur das Prüfen und Auslesen der gecacheten Daten mehr. Die Anzahl der Datenbankabfragen (oder andere rechenintensive Aufgaben) werden trotz eines erhöhten Traffics immer gleich bleiben. Caching bezieht sich nicht nur auf Datenbankabfragen. Zend_Cache ist sehr flexibel und kann für alles Mögliche von Datenbankabfragen bis zu Funktionsaufrufen eingesetzt werden. Einsetzbar ist es bei jeder ladeintensiven Operation Ihrer Applikation, doch sollten Sie die Funktion des Cachings verstehen, damit Sie wissen, wie und wann es eingesetzt werden sollte.
14.2
Die Funktionsweise des Cachings Caching ist ein ökonomischer Weg, um die Geschwindigkeit Ihrer Applikation zu steigern und die Serverlast zu reduzieren. Bei den meisten PHP-Applikationen ist die Aufgabe, die die meisten Ressourcen und Zeit beansprucht, die Durchführung von Datenbankoperationen. Listing 14.1 zeigt einen typischen Code, mit dem man anhand von Zend_Db_Table Informationen aus einer Datenbank auslesen kann.
324
14.2 Die Funktionsweise des Cachings Listing 14.1 Daten ohne Caching aus einer Datenbank auslesen Zend_Loader::loadClass('Product'); $productTable = new Product(); $products = $productTable->fetchAll();
Das ist Zend Framework-Standard-Code, der das Product-Model lädt und fetchAll() aufruft, um eine Liste aller Produkte in der Datenbank auszulesen. Der gesamte Vorgang wird im Flussdiagramm von Abbildung 14.1 gezeigt. Anfrage des Browsers
Datenbankabfrage formulieren
Daten aus Datenbank auslesen
Datenbank
Daten verarbeiten
Antwort an den Browser
Abbildung 14.1 Eine Datenbankabfrage ohne Caching
Bei den meisten Applikationen wird es mehrfache Datenbankabfragen geben – Dutzende oder gar Hunderte. Jede Anfrage bei einer Datenbank verbraucht Prozessorzeit und Speicher. Wenn Sie viele Besucher gleichzeitig auf Ihrer Site haben, können die Serverressourcen sehr schnell verbraucht sein. Bei Situationen mit viel Traffic oder auf Low-Budget-Servern mit minimalen Ressourcen kommt das häufig vor. Listing 14.2 zeigt, wie die Caching-Fähigkeiten von Zend_Cache in den vorigen Code eingebaut werden können.
325
14 Das Caching beschleunigen Listing 14.2 Daten mit Caching aus einer Datenbank auslesen Zend_Loader::loadClass('Zend_Cache'); $frontendOptions = array( // set frontend options Setzt ); Cache$backendOptions = array( Optionen // set backend options ); $query_cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
Erstellt CacheObjekt
Setzt
$cacheName = 'allproducts'; if(!($result = $query_cache->load($cacheName))) { Zend_Loader::loadClass('Product'); Führt ressourcen$productTable = new Product(); intensive Operation $result = $productTable->fetchAll();
eindeutigen Identifikator
aus
$query_cache->save($result, $cacheName);
Wenn möglich, aus Cache laden
Speichert
}
in Cache
Anhand der Methode factory() wird ein Zend_Cache-Cache-Objekt erstellt. Dieses gibt ein Frontend-Cache-Objekt zurück, welches an ein Backend-Objekt angehängt wird. In diesem Fall nutzen wir die „Core“-Frontend-Klasse (Zend_Cache_Core) und die „File“Backend-Klasse (Zend_Cache_Backend_File), was bedeutet, dass die gecacheten Daten auf der Festplatte in Dateien gespeichert werden. Um die Daten in einem Cache zu speichern, brauchen wir einen eindeutigen Namen E. Die Daten aus dem Cache werden über die load()-Methode ausgelesen F. Wenn sich die Daten nicht im Cache befinden (oder verfallen sind), können wir die ressourcenintensive Operation ausführen G und mit save() die Ergebnisse in den Cache speichern (. Der ganze Caching-Vorgang wird im Flussdiagramm in Abbildung 14.2 gezeigt. Anfrage vom Browser
Steht eine gecachete Version zur Verfügung? NEIN
JA
Datenbankabfrage formulieren
Datenbank
Daten aus Cache auslesen
Daten aus Datenbank auslesen
Daten verarbeiten
Antwort an den Browser
Abbildung 14.2 Eine Datenbankabfrage mit Caching
326
Cache-Datei
14.2 Die Funktionsweise des Cachings Auf diese Weise wird die Abfrage nur einmal gestartet (abhängig davon, auf welches Verfallsdatum Ihr Cache eingestellt ist), und der Cache kann die Daten dann Hunderte, Tausende oder Millionen Male ausgeben, ohne dass die Datenbank erneut abgefragt wird, bis der Cache abgelaufen ist. Das Caching beruht auf zwei Prinzipien:
Der eindeutige Identifikator: Wenn das Cache-System prüft, ob ein Cache-Ergebnis schon vorhanden ist, arbeitet es dafür mit dem unique identifier. Es ist äußerst wichtig, darauf zu achten, dass die eindeutigen Identifikatoren tatsächlich eindeutig sind. Andernfalls hätten Sie zwei separate Elemente, die mit dem gleichen Cache arbeiten und miteinander in Konflikt geraten. Am besten ist es, wenn man den Caching-Code für diesen Identifikator nur einmal im Code hat, z. B. in einer Funktion oder einer Methode, um ihn dann bei Bedarf von verschiedenen Stellen aus aufrufen zu können.
Die Verfallszeit: Damit ist das Verfallsdatum für einen Cache gemeint, also der Zeitpunkt, nach dem die Inhalte erneut generiert werden. Wenn die gecacheten Daten sich nicht häufig ändern, können Sie die Ablaufzeit auf 30 Tage setzen. Wenn sich etwas öfter ändert, aber Sie eine Site mit viel Traffic haben, können Sie diesen Zeitraum auf 5 oder 30 Minuten oder eine Stunde setzen. Welchen Zeitraum man nehmen sollte, wird in Abschnitt 14.4 diskutiert. Abbildung 14.3 zeigt, wie ein Caching-System bestimmt, ob die rechenintensive Aufgabe durchgeführt oder das Ergebnis aus dem Cache geladen werden soll. Anfrage vom Browser
Ist eine gecachete Version verfügbar? NEIN
Datenbankabfrage formulieren
JA
JA
Ist die gecachete Version verfallen?
NEIN
Datenbank
Daten aus Datenbank lesen Daten aus Cache lesen
Cache-Datei
Ergebnis in Cache speichern
Daten verarbeiten
Antwort an den Browser
Abbildung 14.3 Der Entscheidungsprozess eines Caching-Systems
Ein Caching-System arbeitet mit dem eindeutigen Identifikator, um nach einem vorhandenen Cache-Resultat zu suchen. Falls es eines gibt, prüft es, ob das Resultat abgelaufen ist.
327
14 Das Caching beschleunigen Ist das nicht der Fall, wird das gecachete Ergebnis zurückgegeben – das bezeichnet man als Cache-Hit. Wenn kein Cache-Ergebnis vorhanden ist oder das existierende verfallen ist, nennt man das einen Cache-Miss. Nach diesem Blick auf die Funktionsweise des Cachings schauen wir uns an, wie man das anhand von Zend_Cache in eine Applikation integriert.
14.3
Die Implementierung von Zend_Cache Die Implementierung von Zend_Cache ist sehr einfach. Sobald Sie entdeckt haben, wie einfach es ist, dann werden Sie damit überall arbeiten wollen! Die Optionen für Zend_Cache teilen sich in zwei Hauptbereiche auf: das Frontend und das Backend. Frontend bezieht sich auf die zu cachende Operation wie einen Funktionsaufruf oder eine Datenbankabfrage. Das Backend bezieht sich darauf, wie das Cache-Ergebnis gespeichert wird. Wie wir in Listing 14.2 gesehen haben, gehört zur Implementierung von Zend_Cache die Instanziierung des Objekts und das Setzen der Optionen für Front- und Backend. Nachdem dies erfolgt ist, führen Sie die Prüfung des Caches aus. Jedes Frontend macht das auf andere Weise, doch im Wesentlichen wird abgefragt, ob ein Cache vorhanden ist. Ist das nicht der Fall, wird mit dem Code weitergemacht, der für die Generierung des Ergebnisses erforderlich ist, das im Cache gespeichert werden soll. Dann wird Zend_Cache angewiesen, das Ergebnis zu speichern. Listing 14.3 zeigt ein Anwendungsbeispiel, um mit Zend_Cache eine Datenbankabfrage anhand eines Models zu cachen. Listing 14.3 Beispiel einer Nutzung von Zend_Cache Zend_Loader::loadClass('Zend_Cache'); $frontendOptions = array( 'lifetime' => 60 * 5, // 5 Minuten 'automatic_serialization' => true, ); $backendOptions = array( 'cache_dir' => BASE_PATH . '/application/cache/', 'file_name_prefix' => 'zend_cache_query', 'hashed_directory_level' => 2, ); $query_cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions); $cacheName = 'product_id_' . $id; if(!($result = $query_cache->load($cacheName))) { Zend_Loader::loadClass('Product'); $productTable = new Product(); $result = $productTable->fetchRow(array('id = ?' => $id)); $query_cache->save($result, $cacheName); }
Die Frontend-Optionen steuern die Arbeitsweise des Caches. Der lifetime-Schlüssel bestimmt beispielsweise, wie lange die gecacheten Daten verwendet werden dürfen, bevor
328
14.3 Die Implementierung von Zend_Cache sie verfallen. Die Backend-Optionen sind für die verwendete Art von Cache-Speicherung typisch. Bei Zend_Cache_Backend_File sind Informationen über das Verzeichnis (cache_ dir) wichtig und darüber, und wie viele Verzeichnisebenen (hashed_directory_level) verwendet werden. Der Rest des Codes ist wie der in Listing 14.2, nur dass dieses Mal der Cache-Name spezifisch für die Produkt-ID ist und die gecacheten Daten sich nur auf ein einziges Produkt beziehen. Schauen wir uns die verfügbaren Zend_Cache-Frontends und die Konfigurationsoptionen im Detail an.
14.3.1 Die Zend_Cache-Frontends Alle Frontends erweitern Zend_Cache_Core, doch weder Zend_Cache_Core noch eines der Frontends werden instanziiert. Stattdessen arbeiten sie mit der statischen Methode Zend_Cache::factory(). Die vier Argumente für diese Methode sind $frontendName (String), $backendName (String), $frontendOptions (assoziatives Array) und $backendOptions (assoziatives Array). Der Befehl lautet wie folgt: $cache = Zend_Cache::factory( $frontendName, $backendName, $frontendOptions, $backendOptions );
Jedes Front- und Backend hat seine eigenen Optionen, die sich auf dessen Arbeitsweise auswirken. Die zentralen Frontend-Optionen finden Sie in Tabelle 14.1. Tabelle 14.1 Die zentralen Frontend-Optionen von Zend_Cache Option
Beschreibung
caching
Standardmäßig auf true gesetzt. Somit werden Sie diese Option wahrscheinlich nicht ändern, können sie aber auf false setzen, falls Sie zu Testzwecken das Caching abschalten wollen. Das ist eine Alternative dazu, den Cache-Test auszukommentieren.
lifetime
Das ist der Zeitraum in Sekunden, nach dem der Cache verfällt und das Resultat erneut generiert wird. Als Standard auf 1 Stunde gesetzt (3600 Sekunden). Kann auf null gesetzt werden, damit der Cache nie verfällt.
logging
Wird dies auf true gesetzt, erfolgt ein Logging über Zend_Log. Standardmäßig auf false gesetzt. Bei true erfolgt ein Performance-Hit.
write_control
Standardmäßig auf true gesetzt. Der Cache wird dann nach dem Schreiben gelesen, um zu prüfen, ob er beschädigt wurde. Sie können diese Option deaktivieren, doch es ist gut, einen weiteren Schutz gegen Korrumpierung zu haben. Von daher empfehlen wir, sie eingeschaltet zu lassen.
329
14 Das Caching beschleunigen Option
Beschreibung
automatic_serialization
Wenn auf true gesetzt, werden die Cache-Daten serialisiert (schlagen Sie die Serialisierungsfunktion im PHP-Manual nach). Damit können komplexe Datentypen wie Arrays oder Objekte gespeichert werden. Wenn Sie nur einen einfachen Datentype wie einen String oder einen Integer speichern, müssen die Daten nicht serialisiert werden, und der Standardwert false kann bleiben.
automatic_cleaning_factor
Die automatische Aufräumvorrichtung leert den abgelaufenen Cache, wenn ein neues Cache-Ergebnis gespeichert wird. Wenn sie auf 0 gesetzt wird, werden verfallene Caches nicht geleert. Auf 1 gesetzt wird nach jedem Schreiben in den Cache aufgeräumt. Wenn Sie diesen Wert auf eine Zahl größer als 1 setzen, wird zufällig nach x Mal geleert, wobei x die von Ihnen eingegebene Zahl ist. Standardmäßig ist der Wert auf 10 gesetzt. Innerhalb von 10 Mal Speichern wird der Cache einmal geleert.
Jedes Frontend ist so entworfen, dass Sie bei Ihrer Applikation unterschiedliche CachingMöglichkeiten haben. Die folgenden Unterabschnitte beschreiben die individuellen Frontends und die verbleibenden Optionen. Bitte beachten Sie, dass alle Beispiele davon ausgehen, dass $backendName, $frontendOptions und $backendOptions bereits definiert worden sind. Somit können Sie Ihre eigenen Optionen bei jedem Frontend austauschen. Die Frontends sind in Tabelle 14.2 aufgelistet. Tabelle 14.2 Die Frontends des Caches Name
Beschreibung
Core
Die Grundlage aller Frontends. Kann aber auch für sich alleine verwendet werden. Arbeitet mit der Klasse Zend_Cache_Core.
Output
Nutzt einen Output-Puffer, um den Output des Codes abzufangen und im Cache zu speichern. Arbeitet mit der Klasse Zend_Cache_Frontend_Output.
Function
Speichert die Ergebnisse der prozeduralen Funktionen. Arbeitet mit der Klasse Zend_Cache_Frontend_Function.
Class
Speichert das Ergebnis der statischen Klassen- oder Objektmethoden. Arbeitet mit der Klasse Zend_Cache_Frontend_Class.
File
Speichert das Ergebnis des Ladens und Parsens einer Datei. Arbeitet mit der Klasse Zend_Cache_Frontend_File.
Page
Speichert das Ergebnis einer Seitenanfrage. Arbeitet mit der Klasse Zend_Cache_Frontend_Page.
Das am häufigsten verwendete Frontend ist Zend_Cache_Core.
330
14.3 Die Implementierung von Zend_Cache 14.3.1.1 Zend_Cache_Core Dies ist die Basisklasse für alle Frontends, aber wir können bei Bedarf direkt darauf zugreifen. Das ist am praktischsten, wenn Variablen wie Strings, Arrays oder Objekte gespeichert werden sollen. Alle Frontends konvertieren das Cache-Ergebnis auf diese Weise zur Speicherung in eine Variable. Die einfachste Verwendung von Zend_Cache_ Core zeigt Listing 14.4. Listing 14.4 Einfache Nutzung von Zend_Cache_Core $frontendName = 'Core'; $cache = Zend_Cache::factory( $frontendName, $backendName, $frontendOptions, $backendOptions ); if (!($data = $cache->load('test'))) { // Hier berechnungsintensive Aufgabe ausführen $cache->save($data, 'test'); }
Man kann mit Zend_Cache_Core besonders gut die Ergebnisse von Datenbankaufrufen speichern, weil es dafür keine spezielle Zend_Cache_Frontend_*-Klasse gibt. Ein Beispiel zeigt Listing 14.5. Listing 14.5 Mit Zend_Cache_Core Datenbankergebnisse speichern $cacheName = 'product_' . $productId; if(!$result = $cache->load($cacheName)) { Zend_Loader::loadClass('Product'); $productTable = new Product(); $result = $productTable->fetchRow(array('id = ?' => $productId)); $cache->save($result, $cacheName); }
Wie Sie sehen, haben wir $cacheName ergänzt. So können Sie die eindeutigen Identifikatoren anhand von Variablen setzen und damit den Cache laden und speichern. Doch beachten Sie bitte, dass der in diesem Beispiel verwendete Identifikator sehr einfach ist, weil wir die Zeile nur basierend auf einer Bedingung abholen. Wenn Sie eine Abfrage mit mehreren Bedingungen durchführen, müssen Sie sich überlegen, wie man zu diesem Zweck einen eindeutigen Identifikator generiert. Wenn der von Ihnen gewählte Identifikator nicht eindeutig genug ist, könnten Sie aus Versehen den gleichen Identifikator für zwei verschiedene Abfragen nehmen, was zu Fehlern führen würde. Einen eindeutigen Identifikator setzen In vielen Fällen können Sie einen eindeutigen Identifikator mit folgendem Code setzen: md5(serialize($conditions));
Damit werden die Bedingungen, z. B. ein Array, in einen einzigen eindeutigen String konvertiert. Sie können alle Ihre Bedingungen dazu in einem Array zusammenführen. Die serialize()-Funktion produziert einen String, der an md5() übergeben wird. Die md5()-Funktion implementiert den MD5-Algorithmus, der eine Repräsentation der serialisierten Daten aus 32 Zeichen produziert.
331
14 Das Caching beschleunigen Der MD5-Algorithmus erstellt einen sogenannten Einweg-Hash (one way hash). Der gleiche Input führt immer zum gleichen Hash-Wert, und es ist extrem selten, dass zwei unterschiedliche Input-Werte zum gleichen Hash führen. Dadurch wird es zu einem sehr guten Mittel, einen langen String auf einen kleineren, eindeutigen Wert zu reduzieren. Die sha1()-Funktion ist eine Alternative, die mit dem SHA1-Algorithmus arbeitet. Sie gleicht MD5 insofern, dass damit ein Einweg-Hash produziert wird, aber das Ergebnis weist 40 Zeichen auf. Das reduziert die Chance noch weiter, dass zwei verschiedene Werte zum gleichen Hash führen. Beim Setzen eigener Identifikatoren ist es wichtig, die Regeln dafür zu befolgen, welche Zeichen darin enthalten sein können. Der Identifikator darf nicht mit „internal-“ beginnen, weil das für Zend_Cache reserviert ist. Identifikatoren dürfen nur a-z, A-Z, 0-9 und _ enthalten. Bei allen anderen Zeichen wird eine Exception geworfen und das Skript abgebrochen. Wie Sie Listing 14.5 entnehmen, wird die $result-Variable mit dem gecacheten Ergebnis gefüllt, wenn es einen Cache-Hit gibt, und diese Variable kann dann behandelt werden, als wäre sie von der Datenbankabfrage zurückgegeben worden. Bei einem Cache-Miss wird die Datenbankabfrage ausgeführt und der $result-Wert erneut gefüllt, bevor das Ergebnis in den Cache gespeichert wird. Auf beide Weisen kann der $result-Wert dann ab hier genau gleich behandelt werden. Anmerkung
Beim Speichern von Objekten in einem Cache (wie etwa den Ergebnissen einer Datenbankabfrage) müssen Sie darauf achten, ob die passende Klasse geladen wurde (z. B. Zend_Db_Table_Row), bevor das Objekt aus dem Cache gelesen wird. Anderenfalls kann das Objekt nicht korrekt rekonstruiert werden, und Ihre Applikation wird wahrscheinlich abstürzen, weil die erwarteten Eigenschaften und Methoden nicht vorhanden sind. Wenn Sie mit Autoloading arbeiten, wird die Klasse automatisch für Sie geladen, wenn sie gebraucht wird. In Abschnitt 3.2.2 finden Sie mehr Infos über das Autoloading.
Möglicherweise ist es für Sie praktisch, wenn der Code für das Caching in der Methode eines Models platziert wird (siehe Listing 14.6). Listing 14.6 Der Caching-Code in einem Model class Product extends Zend_Db_Table_Abstract { protected $_name = 'product'; protected $_primary = 'id'; public function fetchRowById($id) { Zend_Loader::loadClass('Zend_Cache'); $frontendOptions = array ( //... ); $backendOptions = array ( //...
Durch Verschieben des Cache-Codes in die Methode fetchRowById() haben wir nun einen zentralen Platz, um unsere Cache-Optionen zu verwalten. Wir könnten beispielsweise die „Lifetime“ besser an einem Ort ändern als an vielen verschiedenen, einen Bug mit dem eindeutigen Identifikator fixen, das Caching zum Debuggen deaktivieren oder den Cache leeren. Wenn Sie aus der product-Tabelle eine Zeile mit einer speziellen ID brauchen, nehmen Sie nun diesen Code: $productTable->fetchRowById($id);
Sie platzieren diesen Code an mehreren Stellen und müssen sich nicht jedes Mal ums Caching Gedanken machen, denn das ist nun für Sie geregelt. Obwohl Sie mit Zend_Cache_Core den Output Ihres Codes cachen können, gibt es im Zend Framework schon eine Klasse namens Zend_Cache_Frontend_Output, die genau für diesen Zweck gedacht ist. 14.3.1.2 Zend_Cache_Frontend_Output Dieses Frontend arbeitet mit einem Output-Puffer, um den Output aus Ihrem Code zu cachen. Darin wird der gesamte Output, z. B. Echo- und Print-Anweisungen, zwischen den start()- und end()-Methoden abgefangen Er arbeitet mit einem einfachen Identifikator, um zu bestimmen, ob ein Cache-Ergebnis vorhanden ist. Falls nicht, wird er den Code ausführen und den Output speichern. Es gibt keine zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Output (siehe Listing 14.7). Listing 14.7 Die Nutzung von Zend_Cache_Frontend_Output if (!($cache->start('test'))) { echo 'Cached output'; $cache->end(); }
Der Output wird also gecachet, wenn die start()-Methode false zurückgibt (kein valider Cache gefunden). Wenn start() true zurückgibt, wird Zend_Cache_Frontend_Output all das ausgeben, was im Cache gespeichert wurde. Sie müssen sorgfältig darauf achten, dass Sie nicht den gleichen eindeutigen Identifikator für unterschiedlichen Output in separaten Bereichen Ihres Codes einsetzen.
333
14 Das Caching beschleunigen Wenn Sie das Ergebnis einer Funktion speichern wollen, können Sie die Klasse Zend_ Cache_Frontend_Function nehmen. 14.3.1.3 Zend_Cache_Frontend_Function Dieses Frontend speichert das Ergebnis eines Funktionsaufrufs. Es kann einen Funktionsaufruf von einem anderen unterscheiden, indem es die Eingabewerte vergleicht. Wenn die Eingabewerte auf ein vorhandenes Cache-Ergebnis passen, kann es dann eher dieses Ergebnis zurückgeben, als die gleiche Funktion erneut auszuführen. Die Frontend-Optionen für Zend_Cache_Frontend_Function erlauben die zentrale Steuerung des Cachings für einzelne Funktionen. Der Hauptvorteil davon ist, dass wenn Sie beschließen, dass Sie eine bestimmte Funktion nicht cachen wollen, dann müssen Sie nicht jeden Aufruf für diese Funktion ändern, sondern einfach nur die Option. Das mag noch ein wenig verwirrend erscheinen, doch das erklären wir gleich. Tabelle 14.3 zeigt die zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Function. Tabelle 14.3 Zusätzliche Frontend-Optionen für Zend_Cache_Frontend_Function Option
Beschreibung
cacheByDefault
Standardmäßig auf true gesetzt, d. h. alle Funktionen, die Zend_Cache_Frontend_Function durchlaufen, werden gecachet, und Sie müssen nonCachedFunctions setzen, um das für einzelne Funktionen abzuschalten. Wenn dies auf false gesetzt ist, müssen Sie cachedFunctions setzen, um ein Caching für bestimmte Funktionen zu ermöglichen.
cachedFunctions
Wenn cacheByDefault abgeschaltet ist, können Sie hier die Funktionen, die gecachet werden sollen, als Array definieren.
nonCachedFunction s
Wenn cacheByDefault eingeschaltet ist (Standard), können Sie das Caching von Funktionen abschalten, indem Sie sie hier als Array einfügen.
In Listing 14.8 finden Sie das Beispiel eines normalen Aufrufs einer rechenintensiven Funktion ohne Caching. Listing 14.8 Beispiel eines Funktionsaufrufs ohne Caching function intensiveFunction($name, $animal, $times) { $result = ''; for ($i = 0; $i < $times; $i++) { $result .= $name; $result = str_rot13($result); $result .= $animal; $result = md5($result); } return $result; } $result = intensiveFunction('bob', 'cat', 3000);
334
14.3 Die Implementierung von Zend_Cache Um diese Funktion zu cachen, nehmen Sie die call()-Methode von Zend_Cache_ Frontend_Function. Listing 14.9 zeigt, wie Zend_Cache_Frontend_Function in Listing 14.8 eingesetzt wird. Listing 14.9 Beispiel eines Funktionsaufrufs mit Caching function intensiveFunction($name, $animal, $times) { $result = ''; for ($i = 0; $i < $times; $i++) { $result .= $name; $result = str_rot13($result); $result .= $animal; $result = md5($result); } return $result; } Zend_Loader::loadClass('Zend_Cache'); $frontendOptions = array ( ... ); $backendOptions = array ( ... ); $queryCache = Zend_Cache::factory( 'Function', 'File', $frontendOptions, $backendOptions ); $result = $cache->call('intensiveFunction', array('bob', 'cat', 3000));
Das ist exakt das Gleiche, als würde man call_user_func_array() aufrufen, außer dass das Ergebnis gecachet wird. Wenn Sie beschließen, dass Sie diese Funktion nicht mehr länger cachen wollen, könnten Sie das in den Optionen setzen, anstatt den gesamten Code wieder auf den ursprünglichen Aufruf von intensiveFunction() zurückzusetzen: $nonCachedFunctions = array('intensiveFunction');
Wenn Sie das Caching wieder aktivieren wollen, entfernen Sie es einfach aus dem Array. Wenn Sie den Input Ihrer Funktion ändern, wird Zend_Cache_Frontend_Function dies als eindeutigen Identifikator behandeln: $result = $cache->call('intensiveFunction', array('alice,' 'dog', 7)); Zend_Cache_Frontend_Function wird dies separat vom vorigen Funktionsaufruf cachen. Das bedeutet, dass Sie keinen eigenen eindeutigen Identifikator erstellen müssen.
Bitte beachten Sie, dass Sie Zend_Cache_Frontend_Function nicht für den Aufruf statischer Methoden von Klassen nutzen können. Stattdessen müssen Sie mit Zend_Cache_Frontend_Class arbeiten. 14.3.1.4 Zend_Cache_Frontend_Class Dieses Frontend ist ähnlich wie Zend_Cache_Frontend_Function, kann aber die Ergebnisse der statischen Methoden einer Klasse oder das Ergebnis von nicht-statischen Methoden eines Objekts speichern.
335
14 Das Caching beschleunigen Die zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Class finden Sie in Tabelle 14.4. Tabelle 14.4 Die zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Class Option
Beschreibung
cachedEntity
Setzt dies entweder auf die Klasse (für statische Methoden) oder das Objekt (für nicht-statische Methoden). Das ist bei jedem Cache-Aufruf erforderlich.
cacheByDefault
Standardmäßig auf true gesetzt, d. h. alle Methoden, die Zend_Cache_Class_Function durchlaufen, werden gecachet, und Sie müssen nonCachedMethods setzen, um das für einzelne Funktionen abzuschalten. Wenn dies auf false gesetzt ist, müssen Sie cachedMethods setzen, um ein Caching für bestimmte Funktionen zu ermöglichen.
cachedMethods
Wenn cacheByDefault abgeschaltet ist, können Sie hier die Methoden, die gecachet werden sollen, als Array definieren.
nonCachedMethods
Wenn cacheByDefault eingeschaltet ist (Standard), können Sie das Caching von Methoden abschalten, indem Sie sie hier als Array einfügen.
Wenn die Methode durch den Cache aufgerufen wird, behandeln Sie das Cache-Objekt, als wäre es die ursprüngliche Klasse oder Methode, die Sie aufrufen wollten. Zum Beispiel wird $result = $someObject->someMethod(73);
zu $result = $cache->someMethod(73);
Oder eine statische Methode $result = someClass::someStaticMethod('bob');
wird zu $result = $cache->someStaticMethod('bob');
Das Frontend Zend_Cache_Frontend_Class verwendet die Klasse oder das Objekt und den Methoden-Input als eindeutigen Identifikator. Also brauchen Sie keinen zu erstellen. Das nächste praktische Frontend ist Zend_Cache_Frontend_File. 14.3.1.5 Zend_Cache_Frontend_File Dieses Frontend cachet die Ergebnisse des Parsens einer bestimmten Datei. Es ist im Wesentlichen in der Lage zu bestimmen, ob die Datei sich geändert hat, und verwendet das, um zu festzustellen, ob sie geparst werden muss oder nicht. Beim Parsen kann es sich um alles Mögliche handeln; was Sie cachen, ist der Code, der von den Inhalten einer bestimmten Datei abhängt. Das ist im Wesentlichen das Gleiche wie bei Zend_Cache_Core, außer
336
14.3 Die Implementierung von Zend_Cache dass der Cache erst verfällt, wenn die Datei verändert wird, statt nach einem festgelegten Zeitraum. Es gibt eine weitere Frontend-Option für Zend_Cache_Frontend_File, die Sie in Tabelle 14.5 sehen. Tabelle 14.5 Die zusätzliche Frontend-Option für Zend_Cache_Frontend_File Option
Beschreibung
Master_file
Diese Option ist erforderlich und muss den vollständigen Pfad und Namen der zu cachenden Datei enthalten.
Nachdem Sie die Masterdatei definiert haben, können Sie mit Zend_Cache_Frontend_File wie in Listing 14.10 gezeigt arbeiten. Listing 14.10 Beispiel einer Nutzung von Zend_Cache_Frontend_File $filename = 'somefile.txt'; $cacheName = md5($filename); if (!($result = $cache->load($cacheName))) { $data = file_get_contents($filename); $result = unserialize($data); $cache->save($result, $cacheName); }
Dieser Wert für $filename hier wird in die Frontend-Optionen eingespeist. Dann nehmen wir einen MD5-Digest von $filename als eindeutigen Identifikator. Sie können auch verschiedene, mit einer Datei vorgenommene Operationen cachen, z. B. sie in einer XMLDatei laden und die Inhalte in dem einen Bereich durchsuchen, während die Inhalte direkt in einem anderen Bereich ausgegeben werden. Damit Sie beide Operationen cachen können, müssen Sie für jeden Bereich verschiedene eindeutige Identifikatoren verwenden. Wenn die Datei somefile.txt sich jemals ändert, wird Zend_Cache_Frontend_File das anhand des Modifikationsdatums merken, und der Code wird erneut gestartet. Anderenfalls wird das Ergebnis des Codes (nicht die Daten aus der Datei selbst) zurückgegeben. 14.3.1.6 Zend_Cache_Frontend_Page Das Frontend Zend_Cache_Frontend_Page ist so wie Zend_Cache_Frontend_Output, außer dass es den Output basierend auf der Variable $_SERVER['REQUEST_URI'] cachet und optional auch die vom User eingegebenen Daten, die in den Variablen $_GET, $_POST, $_SESSION, $_COOKIE und $_FILES enthalten sind. Sie initialisieren das durch Aufruf der Methode start(), und es wird sich selbständig speichern, wenn die Seite gerendert wird. Sie können den gesamten Code für die Seitenausführung speichern, wenn die InputVariablen zu einem vorhandenen Cache-Ergebnis passen. Die zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Page finden Sie in Tabelle 14.6.
337
14 Das Caching beschleunigen Tabelle 14.6 Die zusätzlichen Frontend-Optionen für Zend_Cache_Frontend_Page Option
Beschreibung
debug_header
Standardmäßig auf false gesetzt, doch wenn Sie die Option auf true setzen, wird der „DEBUG HEADER :This is a cached page !“ ausgegeben. Leider können Sie diese Nachricht nicht ändern. Doch es wird Sie zumindest daran erinnern, dies zu überprüfen, damit Sie sicher sind, dass der Output die gecachete Seite anstatt des Originals ist.
default_options
Dies kann wirklich komplex werden, doch zum Glück können Sie es in den meisten Situationen bei den Standardeinstellungen belassen. Wenn Sie da wirklich tiefer graben müssen, können Sie das assoziative Array mit den folgenden Optionen setzen. cache Der Standard ist true, und die Seite wird gecachet, wenn alle anderen Bedingungen zutreffen. Wenn Sie den Wert auf false setzen, wird die Seite nicht gecachet. cache_with_get_variables Ist standardmäßig auf false gesetzt. Also wird die Seite erneut gerendert, falls es irgendwelche Variablen in $_GET gibt. Wenn Sie den Wert auf true setzen, wird die Seite weiterhin aus dem Cache geladen. Achten Sie sorgfältig darauf, dies auf true zu setzen, falls die $_GETVariablen die Inhalte der Seite verändern.
cache_with_post_variables Das Gleiche wie cache_with_get_variables, aber mit $_POST. cache_with_session_variables Das Gleiche wie cache_with_get_variables, aber mit $_SESSION. cache_with_files_variables Das Gleiche wie cache_with_get_variables, aber mit $_FILES. cache_with_cookie_variables Das Gleiche wie cache_with_get_variables, aber mit $_COOKIE. make_id_with_get_variables Dieser Wert wird standardmäßig auf true gesetzt, wodurch $_GET in den automatischen Generator der eindeutigen Identifikatoren eingebunden wird. Wenn Sie ihn auf false setzen, wird der Generator die anderen Variablen nehmen. Passen Sie bei dem hier gut auf: Wenn die $_GET-Variablen den Output der Seite ändern, könnten Sie CacheKonflikte bekommen, falls Sie den Wert nicht auf true setzen.
338
14.3 Die Implementierung von Zend_Cache Option
default_options (Fortsetzung)
Beschreibung make_id_with_post_variables Das Gleiche wie make_id_with_get_variables, aber mit $_POST. make_id_with_session_variables Das Gleiche wie make_id_with_get_variables, aber mit $_SESSION. make_id_with_files_variables Das Gleiche wie make_id_with_get_variables, aber mit $_FILES. make_id_with_cookie_variables Das Gleiche wie make_id_with_get_variables, aber mit $_COOKIE.
regexps
Dies ist ein sehr leistungsfähiges Feature. Zwar haben Sie höchstwahrscheinlich nur eine Instanz des Frontends, die sich um alle Ihre Seiten kümmert, aber möglicherweise sollen einige Seiten doch anders als andere behandelt werden. Diese Option ist ein assoziatives Array. Der Schlüssel ist der reguläre Ausdruck (regular expression), mit dem Sie die Seite definieren, auf die die Optionen angewendet werden sollen, und der Wert ist ein Array so wie default_options oben. Der reguläre Ausdruck wird mit $_SERVER['REQUEST_URI'] gestartet. Also können Sie alles nehmen, was Sie wollen, doch meist wird es ein einfacher Ausdruck sein, damit er zu den Controllern oder zu den Kombinationen aus Controllern und Actions passt.
Die einfachste Implementierung von Zend_Cache_Frontend_Page sieht wie folgt aus: $cache = Zend_Cache::factory('Page', 'File', $frontendOptions, $backendOptions); $cache->start();
Beachten Sie, dass wenn $cache->start() aufgerufen wird und ein valider Cache vorhanden ist, Ihre Applikation enden wird, sobald die gecachete Seite ausgegeben worden ist. Wenn wir das Caching für einen bestimmten Controller abschalten wollten, dann müssten wir Folgendes in die $frontendOptions einfügen: 'regexps' => array( '^/admin/' => array( 'cache' => false, ), ),
Damit wird das Seiten-Caching für den gesamten Admin-Bereich der Site abgeschaltet. Wenn Sie sich mit regulären Ausdrücken nicht auskennen, werden Sie sicher zurechtkommen, wenn Sie „^“ an den Anfang eines jeden Eintrags stellen. Wenn Sie mehr wissen müssen, googeln Sie nach regulären Ausdrücken oder kurz „regex“. Wenn wir das Caching für admin abschalten wollen, aber die products-Action gecachet bleiben soll, müssen wir Folgendes machen:
Es wird immer die letzte Regel befolgt, falls es Konflikte mit vorigen Regeln gibt, also wird das wie erwartet funktionieren. Wenn die Zeile ^/admin/products/ über der Zeile ^/admin/ stehen würde, dann würde die products-Action nicht gecachet, weil die Zeile ^/admin/ sie überschreiben würde. Nun kennen Sie alle Frontends, und wir machen mit den Zend_Cache-Backend-Klassen weiter.
14.3.2 Zend_Cache-Backends Die Backends definieren die Art, wie die gecacheten Daten gespeichert werden. In den meisten Fällen ist Zend_Cache_Backend_File das beste und am einfachsten einzusetzende Backend. Es arbeitet zum Speichern der Daten mit einfachen Dateien. Einfache Dateioperationen sind generell deutlich schneller als der Zugriff auf eine Datenbank oder in bestimmten Fällen die Durchführung von ressourcenintensiven Aufgaben. Somit eignen sich Dateien perfekt für das Speichern im Cache. Die zusätzlichen Optionen für Zend_Cache_Backend_File finden Sie in Tabelle 14.7. Tabelle 14.7 Die zusätzlichen Optionen für Zend_Cache_Backend_File
340
Option
Beschreibung
cache_dir
Dies ist der vollständige Pfad zu dem Ort, an dem die CacheDateien gespeichert werden. Der Standard lautet /tmp/, doch wir setzen das lieber auf einen Pfad innerhalb unserer Applikation, damit wir die Cache-Dateien bei Bedarf ganz einfach anschauen und verwalten können.
file_locking
Dadurch wird eine exklusive Sperre eingebaut, um einen gewissen Schutz gegen Cache-Korrumpierung zu bieten. Standardmäßig ist die Option eingeschaltet, und es gibt kaum einen Grund, sie abzuschalten, obwohl es eine sehr geringe Verbesserung in der Performance gibt, falls in bestimmten Situationen die Dateisperre nicht unterstützt wird.
read_control
Ist standardmäßig eingeschaltet und fügt einen Steuerungsschlüssel ein (Digest oder Length), anhand dessen die aus dem Cache gelesenen Daten verglichen werden, um sicherzustellen, dass sie zu den gespeicherten Daten passen.
read_control_type
Damit wird der Auslesetyp gesetzt. Als Standard wird crc32() verwendet, kann aber auf md5() oder strlen() gesetzt werden.
14.3 Die Implementierung von Zend_Cache Option
Beschreibung
hashed_directory_level
Manche Dateisysteme kommen ins Rotieren, wenn in einem Verzeichnis besonders viele Dateien enthalten sind. Das kann lästig sein, wenn Sie Dateien auflisten, per FTP darauf zugreifen etc. Außerdem können sich dadurch Statistiken und ähnliche Applikationen aufhängen. Als Schutz davor setzen Sie hashed_directory_level. Das veranlasst das Backend, mehrere Verzeichnisse und Unterverzeichnis zu erstellen, in denen die Cache-Dateien gespeichert werden sollen, damit in jedem Verzeichnis insgesamt weniger Dateien enthalten sind. Als Standard ist der Wert auf 0 gesetzt – also befinden sich alle Dateien in einem Verzeichnis, doch Sie können ihn abhängig davon, wie viele Dateien Sie erwarten, auf 1 oder 2 (oder mehr) Stufen an Unterverzeichnissen setzen. 1 oder 2 sind wahrscheinlich die sichersten Optionen.
hashed_directory_umask
Anhand von chmod() werden die Berechtigungen für die erstellten Verzeichnisse gesetzt. Standardmäßig auf 0700 gesetzt.
file_name_prefix
Damit wird für alle zu erstellenden Cache-Dateien ein Präfix gesetzt. Als Standard ist das auf zend_cache gesetzt. Das ist gut, falls Sie mit einem generischen /tmp/-Verzeichnis zum Speichern arbeiten. Doch wir nehmen lieber ein Präfix, das die mit diesem Backend zu cachenden Elemente beschreibt, z. B. „Abfrage“ oder „Funktion“. Somit wissen wir, welche Dateien mit welcher CacheAktivität zusammenhängen.
Es gibt noch ein paar andere Backends, die in Tabelle 14.8 aufgelistet werden, doch sie zu erklären, würde den Rahmen dieses Buches sprengen, weil sie viel spezialisierter sind und nur das System ändern, das die Cache-Daten speichert. Wenn Sie sich bei einigen dieser anderen Systeme auskennen, sollten Sie problemlos in der Lage sein, Ihr Wissen über Zend_Cache_Backend_File entsprechend einzusetzen. Bitte beachten Sie, dass Sie vielleicht einige Funktionalitäten verlieren, wenn Sie sich für alternative Backends entscheiden. Tabelle 14.8 Die zusätzlichen Zend_Cache_Backend-Klassen Option
Beschreibung
Zend_Cache_Backend_Sqlite
Arbeitet zur Speicherung mit einer SQLite-Datenbank.
Zend_Cache_Backend_Memcached
Arbeitet zur Speicherung mit einem MemcachedServer.
Zend_Cache_Backend_Apc
Arbeitet mit Alternative PHP Cache.
Zend_Cache_Backend_ZendPlatform
Arbeitet mit der Zend-Plattform.
Wenn Sie erst einmal durchgestiegen sind, wie man den Code mit Caching erweitert, müssen Sie entscheiden, wo Sie für optimale Ergebnisse das Caching einbauen.
341
14 Das Caching beschleunigen
14.4
Caching auf verschiedenen Ebenen der Applikation Caching ist zwar ein erstaunlich leistungsfähiges Tool, kann aber auch unkorrekt eingesetzt werden. Die wichtigsten Entscheidungen, die von Ihnen erwartet werden, ist, was gecachet werden soll und wie lange der Cache die Daten vorhalten soll.
14.4.1 Was in den Cache soll Sie müssen beim Caching sehr sorgfältig darauf achten, dass Sie nicht etwas in den Cache stecken, was jedes Mal gestartet werden muss. Wenn Sie beispielsweise rand(), time() oder Datenbank-Inserts oder -Updates im Cache-Code haben, werden diese erst dann ausgeführt, wenn es valide Cache-Daten gibt. Das kann sehr unglücklich sein, wenn Sie sich darauf verlassen, dass das weiter hinten in Ihrem Code passiert. Wenn Sie beispielsweise mit Zend_Cache_Frontend_Page arbeiten und Anfragedetails für statistische Zwecke in der Datenbank speichern, müssen Sie die Datenbankoperationen ausführen, bevor $cache->start() aufgerufen wird. Anderenfalls wird nur ein Eintrag für jeden Verfallszeitraum gemacht, wodurch Ihre Statistiken nicht valide gemacht werden. Doch manchmal wollen Sie auch solche Sachen wie rand() cachen. Wenn Sie zum Beispiel drei zufällige Produkte auf Ihrer Homepage als Produkt-Highlights darstellen, können Sie das wahrscheinlich für mindestens fünf Minuten cachen, ohne dass die User Experience betroffen ist. Tatsächlich wollen Sie es vielleicht 24 Stunden lang cachen, damit es jeden Tag drei neue „Produkte des Tages“ gibt.
14.4.2 Optimale Verfallszeit des Caches Eine der schwierigsten Sachen beim Caching ist die Entscheidung, wie lange die Elemente im Cache aufbewahrt werden sollen. Sie können in Ihrem Code an beliebiger Stelle verschiedene Cache-Objekte einrichten und allen Objekten unterschiedliche Verfallsdaten zuweisen. Es ist eine gute Idee, das möglichst an einem allgemein zugänglichen Ort abzulegen, damit das Caching mit $productTable->fetchRowById() automatisch erledigt wird (wie bereits beschrieben). So ist gewährleistet, dass all Ihre Cache-Optionen wie z. B. das Verfallsdatum konsistent bleiben, und Sie bei Bedarf die Optionen ganz einfach ändern können. Wenn Sie ein Verfallsdatum wählen, kommt es auf den gecacheten Datentyp an und wie oft der sich wahrscheinlich ändern wird und außerdem darauf, wie viel Traffic Sie erwarten. Wenn sich die Daten oft ändern wie z. B. bei Benutzerkommentaren, sollten Sie den Cache auf fünf Minuten setzen. Die Daten werden dann höchstens fünf Minuten alt sein. Wenn Sie eine Site mit hohem Traffic haben, wird dadurch immer noch eine beträchtliche Performance-Verbesserung erzielt, weil Sie vielleicht ein paar Hundert Anfragen in fünf Minuten empfangen.
342
14.5 Cache-Tags Wenn Sie Daten cachen, die sich nicht häufig ändern (z. B. Produktdetails), können Sie den Cache auf sieben Tage setzen. Sie werden allerdings den Cache anhand der Methoden clean() oder remove() von Zend_Cache_Core leeren müssen, falls sich der Preis verändert. In manchen Situationen könnte es eine zweite Anfrage nach der Information geben, bevor die erste Anfrage abgeschlossen und das Ergebnis im Cache gespeichert ist, wenn Ihre lastintensiven Aufgaben lange brauchen. In diesem Fall wird die lastintensive Aufgabe erneut für die zweite Anfrage und alle zusätzlichen Anfragen gestartet, bis ein Ergebnis im Cache gespeichert werden konnte. Bei Situationen mit viel Traffic hängt sich Ihre Site möglicherweise auf, sobald der Cache verfällt. Um dies zu vermeiden, sollten Sie die lastintensive Aufgabe starten und die Daten im Cache ersetzen, sobald die Daten sich verändern, und die lifetime des Caches auf null setzen, damit sie immer valide ist. Somit ist gewährleistet, dass die Daten bei Bedarf immer aus dem Cache gelesen werden und die lastintensive Aufgabe nur dann startet, wenn Sie es wollen. Es gibt einige Daten, die Sie längere Zeit cachen können, z. B. den Output einer aufwendigen mathematischen Funktion, wo der gleiche Input immer zum gleichen Output führt. In diesem Fall können Sie das Verfallsdatum auf 365 Tage oder länger setzen. Wenn Sie das Verfallsdatum eines Caches ändern, wird sich das sofort auswirken.
14.5
Cache-Tags Wenn Sie Daten in den Cache speichern, können Sie ein Tag-Array anhängen. Mit diesen Tags können die Caches, die ein spezielles Tag enthalten, geleert werden. Der Code dafür ist wie folgt: $cache->save($result, 'product_56', array('jim', 'dog', 'tea'));
Falls das jemals erforderlich ist, können Sie den Cache auch programmatisch leeren, wenn Sie z. B. ein Produkt sieben Tage lang cachen und eine Auffrischung des Caches erzwingen wollen, damit sich eine Preisänderung sofort niederschlägt. Das Leeren eines Caches kann sehr spezifisch sein und bis zu einem eindeutigen Identifikator gehen: $cache->remove('product_73');
Oder es kann sehr allgemein sein: $cache->clean(Zend_Cache::CLEANING_MODE_OLD); $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
Diese beiden Befehle leeren alte oder alle Caches (jeder Cache muss dann erneut erstellt werden). Sie können auch Caches leeren, an die spezielle Tags gehängt sind: $cache->clean( Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('dog', 'salami') );
343
14 Das Caching beschleunigen Denken Sie daran, beim Leeren die gleichen Konfigurationsoptionen für das Cache-Objekt zu verwenden wie jene, mit denen Sie die Cache-Daten erstellt haben.
14.6
Zusammenfassung In diesem Kapitel beschäftigten wir uns mit der Implementierung von Caching in Applikationen anhand von Frontends und Backends. Wir schauten uns die Details aller Frontends an, damit Sie die beste Entscheidung darüber treffen, welches Frontend für einen bestimmten Teil Ihrer Applikation am ehesten geeignet ist. Wir haben eine Backend-Option näher angeschaut, doch Sie sollten auch ein paar der anderen Backends untersuchen, um herauszufinden, ob sie besser Ihren Bedürfnissen entsprechen. Mit Caching wird die Performance Ihrer Applikation wesentlich verbessert, und Zend_Cache ist ein ausgezeichnetes Tool dafür. Doch bedarf es womöglich einiger Überlegungen, bis man sicher ist, dass die Informationen korrekt gecachet werden. Wenn sich Ihre Applikation fortentwickelt und sich die Traffic-Muster verändern, werden Sie u.U. feststellen, dass Ihre Cache-Settings überarbeiten werden müssen, um neue PerformanceProbleme zu beheben. Wenn Sie das Glück haben, mit einer Applikation mit extrem hohem Traffic zu arbeiten, werden Sie Zend_Cache mit anderen Technologien zur Verbesserung der Performance kombinieren können, z. B. mit statischen Content-Servern und Server-Clustern als Loadbalancer. Da wir nun wissen, wie man die gute Performance einer Applikation auch bei höherem Traffic sicherstellt, wird es Zeit, sich für die restliche Welt zu öffnen, die einen sehr großen Markt darstellt. Es gibt neben Englisch noch viele andere Sprachen, und so schauen wir uns an, wie Ihre Applikation durch die Features zur Internationalisierung und Lokalisierung weltweit besser auf den Markt gebracht werden kann.
344
15 15 Internationalisierung und Lokalisierung Die Themen dieses Kapitels
Die Unterschiede zwischen dem Übersetzen von Sprachen und von Idiomen Mit Zend_Locale Idiome übersetzen Mit Zend_Translate Sprachen übersetzen Zend_Translate in einer Zend Framework-Applikation integrieren Die meisten Websites sind in nur einer Sprache für ein einziges Land geschrieben, und das macht das Leben sowohl für den Designer als auch den Entwickler einfacher. Manche Projekte brauchen allerdings mehr: In einigen Ländern gibt es mehr als nur eine Sprache (in Wales werden sowohl Englisch als auch Walisisch gesprochen), und manche Websites zielen auf alle Länder ab, in denen die Firma operiert. Um eine Website zu erstellen, die sich an unterschiedliche Länder und Kulturen richtet, müssen an der Applikation wesentliche Änderungen vorgenommen werden, damit verschiedene Sprachen und die in den Ländern jeweils unterschiedlichen Formate für Datums- und Zeitangaben, Währung etc. unterstützt werden. Wir werden uns anschauen, was für eine mehrsprachige Site zu erledigen ist, und uns dann damit beschäftigen, wie die Komponenten Zend_Locale und Zend_Translate des Zend Frameworks den Arbeitsablauf vereinfachen. Zum Schluss implementieren wir eine zweite Sprache in die Places-Website, um zu zeigen, wie man eine lokalisierte Applikation erstellt.
15.1 Die Übersetzung in andere Sprachen und Idiome Bevor wir uns an eine mehrsprachige Website wagen, müssen wir zuerst überlegen, wie sich Sprache und landestypische Gepflogenheiten auf eine Website auswirken. Intuitiv
345
15 Internationalisierung und Lokalisierung denken die meisten Leute nur daran, die Sprache zu wechseln, wenn ein anderes Land auf einer Website unterstützt werden soll. Natürlich müssen wir den gesamten Text in der korrekten Sprache darstellen, aber bei manchen Ländereinstellungen sind auch kulturelle Dinge zu berücksichtigen. Am gravierendsten ist dabei die Formatierung von Datums- und Währungsangaben. Bei Datumsangaben gibt es das berüchtigte Problem, dass in den USA mit dem Format mm/dd/yy gearbeitet wird, während man in Europa dd/mm/yy (oder dd/mm/yyyy) nimmt. Also wird’s schwierig zu bestimmen, für welches Datum 02/03/08 steht: für den 2. März oder den 3. Februar? Ähnlich ist es mit der Währung: In Frankreich setzt man ein Komma, wo man in Großbritannien bei den Dezimalstellen einen Punkt nimmt, und die Franzosen setzen ein Leerzeichen, wo der Brite ein Komma erwartet. Damit sich ein französischer User zu Hause fühlt, sollte €1,234.56 als €1 234,56 dargestellt werden. Die zentrale Steuerungsstelle dafür wird auf einem Computer als Locales (Ländereinstellungen) bezeichnet. Locales sind Strings, die die aktuelle Sprache und das Land definieren, die der User verwendet. Das Locale „en_GB“ meint beispielsweise die englische Sprache im Bereich Großbritannien. Entsprechend ist „es_PR“ das Spanisch, wie es in Puerto Rico gesprochen wird. Im Allgemeinen wird für die Lokalisierung der Sprache nur der erste Teil des Locales verwendet, weil man kaum auf eine Website treffen wird, die sowohl USamerikanisches Englisch als auch britisches Englisch bietet. Schauen wir uns zuerst an, was zur Übersetzung von Webapplikationen in andere Sprachen gehört, und dann kümmern wir uns um den Umgang mit Spracheigenheiten, den Idiomen.
15.1.1 Die Übersetzung in andere Sprachen Die Übersetzung andere Sprachen schließt sowohl eine Änderung am HTML-Design als auch an der Erstellung der Website und am PHP-Code ein, mit dem sie läuft. Die offensichtlichste Änderung ist natürlich, dass jeder String, den der User zu Gesicht bekommt, in der korrekten Sprache verfasst sein muss. Also muss beim Design auch die korrekte Größenbemessung der Textbereiche beachtet werden. Dazu gehört auch jeder Text, der in einer Grafik eingebettet ist. Um mehrere Sprachen unterstützen zu können, müssen also die Bilddateien in allgemeingültige und nur für bestimmte Sprachen gültige Bilder separiert werden, falls Text in Bildern vorkommt. Es gibt mehrere Methoden, um die eigentliche Übersetzung vorzunehmen, doch alle laufen im Grunde auf dasselbe hinaus: Jeder auf der Site dargestellte String muss einem String in der Zielsprache zugeordnet werden. Daraus folgt, dass die Strings von einem professionellen Übersetzer neu geschrieben werden müssen, weil es kaum eine 1:1-Zuordnung von einer Sprache in die andere gibt. Systeme, die Industriestandards genügen, wie z. B. gettext(), besitzen viele Tools, die einem Übersetzer dabei helfen, Phrasen oder Redewendungen in der Applikation zu übersetzen, ohne dass er irgendetwas über das Programmieren weiß.
346
15.2 Die Arbeit mit Zend_Locale und Zend_Translate
15.1.2 Die Übersetzung von Idiomen Die für eine Website offensichtlichsten Spracheigenheiten sind die Formatierung von Datums- und Währungsangaben. Bei PHP wird der Support für Locales über die Funktion setlocale() gesetzt. Wenn das Locale gesetzt ist, arbeiten alle Funktionen damit, für die die Locales von Belang sind. Das bedeutet, dass strftime() mit der korrekten Sprache für die Monate des Jahres arbeitet, und dass money_format() die Komma- und Punktzeichen an den richtigen Stellen setzt, wo sie in der jeweiligen Sprache erwartet werden. Ein Haken dabei ist, dass setlocale() nicht thread-sicher ist und dass die Strings, die Sie setzen müssen, bei den verschiedenen Betriebssystemen nicht einheitlich sind. Also müssen Sie sorgfältig darauf achten. Außerdem gibt es manche Funktionen, die nicht auf allen Betriebssystemen verfügbar sind, wie z. B. money_format() unter Windows. Die Komponente Zend_Locale soll dieses Problem beheben (außerdem enthält sie noch weitere Funktionalitäten wie Normalisierung). Nun kennen wir uns bei der Internationalisierung (internationalization, im englischen Sprachraum auch I18N genannt, weil sich zwischen I und N 18 Buchstaben befinden) und der Lokalisierung (localization oder L10N) schon ein wenig besser aus und können uns damit beschäftigen, wie das Zend Framework die Erstellung einer internationalen Website vereinfacht. Wir beginnen, indem wir uns anschauen, wie Zend_Locale Zahlen und Datumsangaben konvertiert, bevor wir mit der Funktionalität von Zend_Translate für übersetzten Text weitermachen.
15.2 Die Arbeit mit Zend_Locale und Zend_Translate und Zend_Translate sind die zentralen Komponenten im Zend Framework für das Bereitstellen einer Website in mehreren Sprachen. Andere Komponenten, die die Locales beachten, sind Zend_Date und Zend_Currency. Schauen wir uns zuerst Zend_Locale an. Zend_Locale
15.2.1 Die Locales mit Zend_Locale setzen Die Wahl des korrekten Locales ist ganz einfach: $locale = new Zend_Locale('en_GB');
Damit wird ein Locale-Objekt für die englische Sprache in der Region Großbritannien erstellt. Das bedeutet, dass ein Locale immer aus den beiden Teilen Sprache und Region besteht. Wir müssen beide kennen, bevor wir den Locale-String festlegen können, wenn eine Instanz von Zend_Locale erstellt wird. Wir erstellen wie folgt ein Zend_LocaleObjekt für das Locale des Browsers: $locale = new Zend_Locale();
347
15 Internationalisierung und Lokalisierung Mit dem locale-Objekt können dann Listen von allgemeinen Strings übersetzt werden, z. B. Länder, Maßeinheiten und Zeitinformationen wie die Namen von Monaten oder Wochentagen. Wir lesen mit diesem Code auch die Sprache und die Region aus: $language = $locale->getLanguage(); $region = $locale->getRegion();
Dann können wir natürlich diese Informationen nutzen, damit Websites mit der korrekten Landessprache sowie der richtigen Formatierung von Datums-, Zeit- und Währungsangaben ausgestattet werden – so fühlen sich unsere User gleich wie zu Hause. Schauen wir uns zuerst die Zahlen an. 15.2.1.1 Der Umgang mit Zahlen Das augenfälligste regionale Problem mit Zahlen ist, dass in manchen Ländern das Komma zur Trennung der Dezimalstellen verwendet wird und in anderen der Punkt. Wenn Sie bei Ihrer Website den Usern die Eingabe einer Zahl erlauben, müssen Sie diese ggf. konvertieren. Das bezeichnet man als Normalisierung. Nehmen wir ein Formular, in dem man seine monatlichen Versicherungskosten eintragen kann, um sich ggf. ein günstigeres Angebot einzuholen. Ein deutscher User würde dann 3.637,34 eingeben, was Sie dann auf 3637.34 normalisieren müssten. Das erledigen Sie mit dem Code in Listing 15.1. Listing 15.1 Die Normalisierung von Zahlen anhand von Zend_Locale $locale = new Zend_Locale('de_DE'); $number = Zend_Locale_Format::getNumber('3.637,34', 'locale' => $locale)); print $number;
Legt Deutsch als Locale fest
Gibt die Zahl „3637.34“ aus
Wir können die Zahl dann wie gewünscht weiterverarbeiten und ggf. dem User eine Zahl darstellen. In diesem Fall müssen wir erneut die Zahl passend zum Standort des Users formatieren, und dafür nehmen wie die Funktion toNumber() von Zend_Locale (siehe Listing 15.2). Listing 15.2 Die Lokalisierung von Zahlen anhand von Zend_Locale $locale = new Zend_Locale('de_DE'); Legt Deutsch $number = Zend_Locale_Format::toNumber(2435.837, als Locale fest array('precision' => 2, Rundet auf zwei 'locale' => $locale));
Dezimalstellen ab
print $number;
Gibt die Zahl „2.435,84“ aus
Der Parameter precision ist optional und rundet die jeweilige Zahl auf die angegebene Anzahl der Dezimalstellen ab oder auf.
348
15.2 Die Arbeit mit Zend_Locale und Zend_Translate Das waren also die Grundlagen von Zend_Locale. Es kann weitere umfassende Aufgaben übernehmen, einschließlich der Übersetzung zwischen unterschiedlichen Zahlensystemen wie z. B. von arabischen zu römischen Zahlen. Auch die Normalisierung und Lokalisierung von Integern und Gleitkommazahlen wird unterstützt. Weitere Informationen über diese Funktionen finden Sie im Manual. 15.2.1.2 Datum und Zeit mit Zend_Locale Die Formatierung von Datums- und Zeitangaben fällt auch in die Zuständigkeit von Diese Klasse operiert zusammen mit Zend_Date, um das Lesen und Schreiben von Zahlen umfassend zu unterstützen. Steigen wir gleich ein mit der Normalisierung des Datums, denn wie bei Zahlen wird das Datum in verschiedenen Regionen der Welt unterschiedlich geschrieben. Außerdem schreibt natürlich jeder die Namen der Monate und Wochentage in seiner Landessprache. Zend_Locale.
Nehmen wir den 2. März 2007. In Großbritannien wird das als 2/3/2007 geschrieben, in den USA als 3/2/2007. Damit wir mit den vom User eingegebenen Daten arbeiten können, müssen wir sie normalisieren, und dazu nehmen wir getDate() (siehe Listing 15.3). Listing 15.3 Die Normalisierung von Datumsangaben anhand von Zend_Locale $locale = new Zend_Locale('en_US'); $date = Zend_Locale_Format::getDate('3/2/2007', array('locale' => $locale)); print $date['month'];
Legt als Locale USA fest
Gibt „3“ für März aus
Wie gewöhnlich erstellen wir ein locale-Objekt für die korrekte Sprache und Region und verwenden es mit der Funktion getDate(). Hier steht das Locale en_US für die USA, also bestimmt getDate() korrekt, dass es sich beim Monat um März handeln muss. Wenn das Locale zu en_GB geändert wird, wäre es der Februar. Entsprechend können wir mit checkDateFormat() gewährleisten, dass der empfangene Datums-String für das Locale valide ist, und nachdem wir die Datumsinformation in ihre Komponenten aufgeteilt haben, können wir sie auf beliebige Weise manipulieren. Nun haben Sie einen Eindruck von der Arbeit mit Zend_Locale, damit sich auch unsere internationalen Besucher zu Hause fühlen, und wir wollen uns die Fähigkeit von Zend_Translate anschauen, die Site in verschiedenen Sprachen zu präsentieren.
15.2.2 Übersetzung mit Zend_Translate Wie bereits erläutert, muss bei der Übersetzungsarbeit für eine Website zumindest jeder dargestellte String eine übersetzte Version bekommen. Der übliche Weg läuft dafür über gettext(), das sehr leistungsfähig, aber auch recht kompliziert ist. Zend_Translate unterstützt das Format gettext(), aber auch andere bekannte Formate wie Arrays, CSV, TBX, Qt, XLIFF und XmlTm. Zend_Translate ist außerdem thread-sicher, was sehr hilfreich sein kann, wenn Sie mit einem Multithread-Webserver wie z. B. IIS arbeiten.
349
15 Internationalisierung und Lokalisierung unterstützt anhand eines Adaptersystems verschiedene Eingabeformate. Dieser Ansatz ist im Zend Framework sehr verbreitet und erlaubt, dass bei Bedarf weitere Adapter eingefügt werden können. Wir schauen uns zuerst den Array-Adapter an, weil dies ein sehr einfaches und leicht erlernbares Format ist. Am häufigsten wird er mit Zend_Cache eingesetzt, um die Übersetzungen von einem Eingabeformat in ein anderes zu cachen. Zend_Translate
ist in der Verwendung sehr einfach. In Listing 15.4 geben wir Text anhand von ganz gewöhnlichem PHP aus und wiederholen diese Übung in Listing 15.5 mit dem Array-Adapter von Zend_Translate. In diesem Fall machen wir uns das Leben einfacher, indem wir in Großbuchstaben „übersetzen. Zend_Translate
Listing 15.4 Standard-PHP-Output
Gibt Datum im britischen Format aus
print "Welcome\n"; print "=======\n"; print "Today's date is " . date("d/m/Y") . "\n";
Listing 15.4 ist ein sehr einfaches Stück Code, das drei Textzeilen darstellt, vielleicht für ein Befehlszeilenskript. Für eine Übersetzung müssen wir ein Array von Übersetzungsdaten für die Zielsprache erstellen. Das Array besteht aus Identifizierungsschlüsseln, die dem eigentlichen Text zugeordnet sind, der dargestellt werden soll. Diese Schlüssel können ganz beliebig gewählt werden, aber es vereinfacht die Geschichte, wenn es sich dabei im Wesentlichen um das Gleiche wie bei der Ursprungssprache handelt. Listing 15.5 Übersetzte Version des Listings 15.4
Listing 15.5
Translated version of listing 15.4
$data = array(); $data['hello'] = 'WELCOME'; $data['today %1$s'] = 'TODAY\'S DATE IS %1$s';
In diesem Beispiel nehmen wir die Übersetzung anhand der Funktion _() vor n. Das ist ein sehr gebräuchlicher Funktionsname in vielen Programmiersprachen und ÜbersetzungFrameworks. Diese Funktion wird sehr häufig eingesetzt, und es ist im Quellcode weniger störend, wenn sie kurz ist. Wie Sie sehen, unterstützt die Funktion _() auch den Einsatz von printf()-Platzhaltern, damit Sie dynamischen Text an die richtige Stelle innerhalb eines String einbetten können o. Das aktuelle Datum ist ein gutes Beispiel, weil im Deutschen so formuliert wird: „Das heutige Datum ist der {datum}“, aber in einer anderen Sprache könnte das Idiom auch „Der {datum} ist das heutige Datum“ lauten. Durch den Einsatz von printf()-Platzhaltern können wir die dynamischen Daten an die für das verwendete Sprachkonstrukt richtige Stelle verschieben.
350
15.3 Eine zweite Sprache für die Places-Applikation Der Array-Adapter ist hauptsächlich für sehr kleine Projekte sinnvoll, bei denen PHPEntwickler die Übersetzungs-Strings aktualisieren. Bei einem großen Projekt sind die Formate gettext() oder CSV weitaus praktischer. Bei gettext() wird der Übersetzungstext in .po-Dateien gespeichert, die am besten über einen spezialisierten Editor wie poEdit (ein Open Source-Programm) verwaltet werden. Diese Applikationen bieten eine Liste mit Strings der Ursprungssprache, und neben jeden String kann der Übersetzer die Entsprechung in der Zielsprache eintippen. So kann man relativ einfach Übersetzungsdateien erstellen, die vom Quellcode der Website komplett unabhängig sind. Der Prozess, um mit Zend_Translate an gettext()-Quelldateien zu kommen, ist so einfach wie die Auswahl eines anderen Adapters (siehe Listing 15.6). Listing 15.6 Zend_Translate verwendet den gettext()-Adapter. $filename = 'translations/uppercase.po'; $translate = new Zend_Translate('gettext', $filename, 'en');
Wie Sie sehen, wird das Übersetzungsobjekt auf genau gleiche Weise verwendet, egal welcher Adapter für die Übersetzungsquelle zum Einsatz kommt. Schauen wir uns nun an, wie wir das Gelernte in einer echten Applikation umsetzen. Wir adaptieren die Places-Applikation so, dass sie zwei Sprachen unterstützt.
15.3 Eine zweite Sprache für die Places-Applikation Für unser Ziel, Places mehrsprachig zu machen, müssen wir die Benutzerschnittstelle in der Sprache des Betrachters präsentieren. Wir nehmen die gleichen View-Templates für alle Sprachen und achten darauf, dass alle Phrasen entsprechend übersetzt werden. Bei jeder Sprache wird eine eigene Übersetzungsdatei gespeichert, und in unserem Fall nehmen wir den Array-Adapter, um die Sache einfach zu halten. Wenn der User eine Sprache haben will, für die wir keine Übersetzung vorhalten, werden wir Englisch nehmen. Das Resultat sehen Sie in Abbildung 15.1, das die deutsche Version von Places zeigt.
351
15 Internationalisierung und Lokalisierung
Abbildung 15.1 Der Text auf der deutschen Version von Places ist übersetzt, doch es werden die gleichen View-Templates verwendet, um sicherzugehen, dass das Einfügen weiterer Sprachen nicht in zuviel Arbeit ausartet.
Dies sind die zentralen Schritte, um Places mehrsprachig zu machen:
Wir ändern den Standard-Router, damit ein Sprachenelement unterstützt wird. Wir erstellen ein Front-Controller-Plug-in, um ein Zend_Translate-Objekt zu erstellen und die korrekte Sprachdatei zu laden. Es wird auch ein Zend_Locale-Objekt erstellen.
Wir aktualisieren die Controller und Views, um Text zu übersetzen. Wir beginnen damit, wie man den Router des Front-Controllers auf die Mehrsprachigkeit vorbereitet, damit der User eine Sprache auswählen kann.
15.3.1 Die Auswahl der Sprache Zuerst muss festgelegt werden, wie die Sprache des Users bestimmt wird. Die einfachste Lösung besteht darin, anhand der getLanguage()-Funktion von Zend_Locale den Browser zu fragen, und das dann in der Session zu speichern. Bei diesem Ansatz gibt es ein paar Probleme. Erstens verlassen sich Sessions auf Cookies. Also müsste der User Cookies aktiviert haben, damit er die Site in einer anderen Sprache lesen kann. Zweitens führt es zu einem Overhead, wenn für jeden User eine Session erstellt wird – das wollen wir uns nicht zumuten. Drittens würden Suchmaschinen wie Google nur die englische Version der Site sehen.
352
15.3 Eine zweite Sprache für die Places-Applikation Um diese Probleme zu lösen, sollte der Code für die angebotenen Sprachen im URL enthalten sein, und da können wir ihn an zwei Stellen ablegen: in der Domäne oder im Pfad. Mit der Domäne müssten wir die relevanten Domänen kaufen, z. B. placestotakethekids.de, placestotakethekids.fr usw. Diese länderspezifischen Domänen bieten eine sehr einfache Lösung, und für den kommerziellen Betrieb entnehmen Ihre Kunden daraus, dass Sie es mit dem Business in den jeweiligen Ländern ernst meinen. Aber das könnte zu anderen Problemen führen, wenn z. B. der Name der Domäne im Land der Wahl nicht verfügbar ist (beispielsweise gehört apple.co.uk nicht zu Apple Inc.), und bei manchen länderspezifischen Domänen müssen Sie nachweisen, dass Sie eine Niederlassung in diesem Land führen, um den jeweiligen Domänennamen erwerben zu können. Eine Alternative wäre, den Sprachencode als Teil des Pfades einzubinden, z. B. www.placestotakethekids. com/fr für Französisch und www.placestotakethekids.com/de für Deutsch. Für dieses Vorgehen entscheiden wir uns. Weil wir für jeden Sprachencode die vollständigen Locales nehmen wollen, müssen wir eine Zuordnung von dem im URL verwendeten Sprachencode auf den kompletten LocaleCode vornehmen. Beispielsweise soll /en auf en_GB gemappt werden, /fr auf fr_FR usw. für alle unterstützten Sprachen. Wir nehmen unsere INI-Konfigurationsdatei, um diese Zuordnung zu speichern (siehe Listing 15.7). Listing 15.7 Die Locale-Informationen in config.ini setzen languages.en = en_GB languages.fr = fr_FR languages.de = de_DE
Nun steht im Objekt $config, das in der Bootstrap-Klasse und in der Zend_Registry gespeichert wurde, die Liste der validen Sprachencodes und den damit verknüpften Locales zur Verfügung. Wir können die Liste der unterstützten Sprachencodes wie folgt auslesen: $config = Zend_Registry::get('config'); $languages = array_keys($config->languages->toArray());
Um die Sprachencodes innerhalb der Adresse zu verwenden, müssen wir das RoutingSystem auf den zusätzlichen Parameter einstellen. Der Standardrouter interpretiert Pfade in der Form /{modul}/{controller}/{action}/{andere_parameter}
wobei {modul} optional ist. Ein typischer Pfad für Places ist /place/index/id/4
Damit wird die index-Action des place-Controllers mit dem auf 4 gesetzten id-Parameter aufgerufen. Für die mehrsprachige Site müssen wir die Sprache als ersten Parameter einführen, damit der Pfad nun wie folgt aussieht: /{sprache}/{controller}/{action}/{andere_parameter}
353
15 Internationalisierung und Lokalisierung Wir nehmen den Standardcode aus zwei Zeichen für die Sprache. Also sieht ein typischer Pfad für die deutsche Version von Places so aus: /de/place/index/id/4
Um das zu ändern, müssen wir eine neue Weiterleitungsregel implementieren und die Standardroute damit ersetzen. Der Front-Controller kümmert sich nun eigenverantwortlich darum, dass die korrekten Controller und Actions aufgerufen werden. Das wird in der runApp()-Methode der Bootstrap-Klasse erledigt (siehe Listing 15.8). Listing 15.8 Eine neue Weiterleitungsregel für den Sprachensupport implementieren
Lädt aus config $config = Zend_Registry::get('config'); Liste der erlaubten $languages = array_keys($config->languages->toArray()); Sprachencodes $zl = new Zend_Locale(); $lang = in_array($zl->getLanguage(), $languages) Verwendet Browser? $zl->getLanguage() : 'en'; Sprachencode, falls er // add language to default route $route = new Zend_Controller_Router_Route( ':lang/:controller/:action/*', array('controller'=>'index', 'action' => 'index', 'module'=>'default', 'lang'=>$lang)); $router = $frontController->getRouter(); $router->addRoute('default', $route); $frontController->setRouter($router);
in erlaubter Liste ist
Erstellt neue Route
Aktualisiert Router mit neuer Route
Über Zend_Controller_Router_Route definieren wir die Route anhand des Doppelpunkts. Dabei werden zuerst die variablen Sprachenteile definiert, dann der Controller, die Action und das Sternchen, was für „alle anderen Parameter“ steht n. Wir definieren auch die Standardwerte für jeden Teil der Route, falls sie fehlen sollten. Weil die Standardroute durch die neue ersetzt wird, behalten wir die gleichen Standardeinstellungen, damit die index-Action des index-Controllers bei einer leeren Adresse aufgerufen wird. Wir setzen die Standardsprache auf diejenige des Browsers, wie sie durch die getLanguage()-Methode von Zend_Locale bestimmt wird. Wenn wir das allerdings als Standard setzen, bedeutet das, dass diese Wahl bevorzugt wird, wenn der User eine bestimmte Sprache auswählt. Damit können Personen, die mit einem spanischen Browser arbeiten, die Site beispielsweise in Englisch lesen. Nachdem die Weiterleitung nun funktioniert, müssen wir die Übersetzungsdateien laden. Das muss nach der Weiterleitung, doch vor den Action-Methoden erfolgen. Der dispatchLoopStartup()-Methoden-Hook eines Front-Controller-Plug-ins ist das ideale Vehikel, um diese Arbeit auszuführen.
354
15.3 Eine zweite Sprache für die Places-Applikation
15.3.2 Das Front-Controller-Plug-in LanguageSetup Ein Front-Controller-Plug-in hat verschiedene Hooks, also Funktionen, mit denen es sich in die verschiedenen Phasen des Dispatching-Vorgangs einklinkt. In diesem Fall sind wir am dispatchLoopStartup()-Hook interessiert, weil wir die Sprachdateien laden wollen, nachdem das Routing erfolgt ist, aber der soll nur einmal pro Anfrage aufgerufen werden. Das Plug-in LanguageSetup wird in der Places-Library gespeichert und richtet sich nach den Namensrichtlinien des Zend Frameworks, um Zend_Loader nutzen zu können. Der vollständige Name der Klasse lautet Places_Controller_Plugin_LanguageSetup, und sie wird in library/Places/Controller/Action/Helper/LanguageSetup.php gespeichert. Hier folgen die Hauptfunktionen, die sie ausführt:
Sie lädt Sprachdateien, die Übersetzungs-Arrays enthalten. Sie instanziiert das Zend_Translate-Objekt der gewählten Sprache. Sie weist dem Controller und der View den Sprachen-String und das Zend_TranslateObjekt zu. All das wird in der dispatchLoopStartup()-Methode erledigt. Damit sie ihre Arbeit ausführen kann, muss sie das Verzeichnis, in dem die Sprachdateien gespeichert sind, und auch die Liste der verfügbaren Sprachen kennen. Diese Information findet sich in der Klasse Bootstrap, die wir daher anhand ihres Konstruktors an das Plug-in übergeben. Der Plug-in-Konstruktor erwartet zwei Parameter: das Verzeichnis, in dem sich die Übersetzungsdateien befinden, und die Liste der Sprachen aus der Datei config. Wir könnten das Plug-in diese Werte herausfinden lassen, doch wir überlassen lieber der BootstrapKlasse das spezielle Wissen über das Verzeichnissystem, damit bei etwaigen Änderungen diese nur an einer Stelle vorgenommen werden müssen. Entsprechend könnte das Plug-in die Liste der Sprachen aus dem Zend_Config-Objekt direkt auslesen, doch damit entstünde eine unnötige Kopplung mit dieser Komponente. Beginnen wir mit der Erstellung des Plug-ins LanguageSetup. Der erste Teil ist der Konstruktor (siehe Listing 15.9). Listing 15.9 Das Front-Controller-Plug-in LanguageSetup class Places_Controller_Plugin_LanguageSetup extends Zend_Controller_Plugin_Abstract { protected $_languages; protected $_directory; public function __construct($directory, $languages) { $this->_dir = $directory; Speichert in $this->_languages = $languages; Elementvariablen }
355
15 Internationalisierung und Lokalisierung public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request) { }
Hier erfolgt die eigentliche Arbeit
}
Wir müssen das neue Plug-in beim Front-Controller in der runApp()-Methode der Bootstrap-Klasse registrieren. Das wird auf die gleiche Weise erledigt wie die Registrierung des ActionSetup-Plug-ins aus Kapitel 3 und sieht so aus: $frontController->registerPlugin( new Places_Controller_Plugin_LanguageSetup( ROOT_DIR . '/application/configuration/translations', $config->languages->toArray()));
Wir nutzen ROOT_DIR, um das Übersetzungsverzeichnis absolut anzugeben. Nach erfolgreicher Registrierung des Plug-ins und der Speicherung der erforderlichen Daten in lokalen Elementvariablen schreiben wir die dispatchLoopStartup()-Methode, mit der die Locale- und Übersetzungsobjekte eingerichtet werden. Dies sehen Sie in Listing 15.10. Listing 15.10 Die dispatchLoopStartup()-Methode von LanguageSetup public function dispatchLoopStartup( Zend_Controller_Request_Abstract $request) Liest gewählte { Sprache aus $lang = $this->getRequest()->getParam('lang'); if (!in_array($lang, Gewährleistet, dass array_keys($this->_languages))) { gewählte Sprache $lang = 'en'; erlaubt ist }
$localeString = $this->_languages[$lang]; $locale = new Zend_Locale($localeString);
Richtet LocaleObjekt ein
$file = $this->_dir . '/'. $localeString . '.php'; if (file_exists($file)) { $translationStrings = include $file; } else { $translationStrings = include $this->_dir . '/en_GB.php'; }
Lädt
Übersetzungsdatei
if (empty($translationStrings)) { throw new Exception('Missing $translationStrings in language file'); } $translate = new Zend_Translate('array', $translationStrings, $localeString);
15.3 Eine zweite Sprache für die Places-Applikation Der Code in dispatchLoopStartup() besteht ebensoviel aus Fehlerprüfung wie aus eigentlichem Code, was nicht unüblich ist. Zuerst erfassen wir die Sprache, die der User gewählt hat. Das passiert im Request-Objekt, und wir können somit über getParam() auf den Wert zugreifen n. Bevor wir eine Sprachdatei laden, müssen wir zuerst prüfen, ob die gewählte Sprache vorhanden ist o, weil der User theoretisch einen beliebigen Text innerhalb des Sprachenelements des Adresspfads eintippen kann. Weil wir nur eine begrenzte Anzahl von Sprachdateien im Übersetzungsverzeichnis haben, prüfen wir, ob die Wahl des Users verfügbar ist. Falls nicht, nehmen wir stattdessen Englisch. Entsprechend prüfen wir außerdem das Vorhandensein der Datei p, wenn die Sprache gültig ist, um spätere Fehler zu vermeiden, und laden die Datei über eine einfache include-Anweisung. Wir gehen davon aus, dass die Sprachdatei ein Array zurückgibt, das wir $translationStrings zuweisen. Das Array enthält die eigentliche Übersetzung, und somit werfen wir eine Exception, falls dieses Array nicht existiert. Nach Abschluss der Fehlerprüfung instanziieren wir ein neues Zend_Translate-Objekt q. Schließlich registrieren wir alles in Zend_Registry, damit die Information später verwendet werden kann r. Wenn das Zend_Translate-Objekt in der Registry registriert wird, heißt das auch, dass Zend_Form und Zend_Validate damit arbeiten werden, wenn Feldbezeichnungen, Schaltflächen und Validierungsfehlermeldungen in den Formularen übersetzt werden. Wie wir bereits bei Zend_Translate entdeckt haben, unterstützt das System mehrere Adapter, durch die für die Übersetzungs-Strings verschiedene Eingabequellen möglich werden. Bei Places haben wir uns für Arrays entschieden, weil das für den Anfang am einfachsten ist, doch wenn die Site deutlich größer wird, könnte man auch einfach auf gettext() wechseln (dann müssen nur bei dieser init()-Methode Veränderungen vorgenommen werden). In der nächsten Phase nutzen wir das translate-Objekt, um unsere Website zu übersetzen.
15.3.3 Die View übersetzen Damit unsere Website mehrsprachig wird, muss der gesamte englische Text auf jeder Seite geändert werden, damit er die _()-Methode von Zend_Translate durchläuft. Zend Framework bietet die View-Hilfsklasse translate, um das zu erleichtern. Diese muss auf ein Zend_Translate-Objekt zugreifen können, und das geht am einfachsten, indem man eines unter dem Schlüssel „Zend_Translate“ bei Zend_Registry registriert, wie wir das in Listing 15.10 gemacht haben. In den View-Skripten ändern wir das ursprüngliche
Recent reviews
auf der Homepage in translate('Recent reviews'); ?>. Schauen wir uns das auf der Homepage im Einsatz an. Listing 15.11 zeigt den oberen Teil des ViewTemplates index.phtml für die Homepage vor der Lokalisierung.
357
15 Internationalisierung und Lokalisierung Listing 15.11 Der obere Teil des nicht lokalisierten index.phtml
escape($this->title);?>
Welcome to <em>Places to take the kids! This site will help you to plan a good day out for you and your children. Every place featured on this site has been reviewed by people like you, so you'll be able to make informed decisions with no marketing waffle!
Recent reviews
Wie Sie sehen, ist mit Ausnahme des Titels der gesamte Text im View-Template fest kodiert, und das müssen wir ändern (siehe Listing 15.12). Listing 15.12 Die lokalisierte Version von index.phtml
escape($this->translate($this->title));?>
translate('welcome-body'); ?>
Verwendet einen
translate('Recent reviews'); ?>
einfachen Schlüssel
für den langen Text
Mit dem lokalisierten Template durchläuft jeder String die View-Hilfsklasse translate(). Für den sehr langen Textkörper nehmen wir einen einfachen Schlüssel n, damit das Template leichter verständlich ist und die Sprachdatei einfacher wird. Die relevanten Teile der Sprachdateien für Englisch und Deutsch stehen in den Listings 15.13 und 15.14. Die vollständigen Dateien finden Sie im begleitenden Quellcode. Listing 15.13 Die englische Übersetzungsdatei en_GB.php 'Welcome to Places to take thekids!', 'welcome-body' => 'Welcome to Places to take the kids! This site will help you to plan a good day out for you and your children. Every place featured on this site has been reviewed by people like you, so you\'ll be able to make informed decisions with no marketing waffle!', 'Recent reviews' => 'Recent reviews', );
Die entsprechende Datei auf Deutsch steht in Listing 15.14. Listing 15.14 Die deutsche Übersetzungsdatei de_DE.php 'Willkommen bei Places to take the kids!', 'welcome-body' => 'Willkommen bei Places to take the kids! Diese Website unterst¨tzt Sie bei der Planung eines schönen Ausfluges mit Ihren Kindern. Alle auf dieser Website präsentierten Orte haben solche Menschen wie du und ich geprüft, damit Sie sich ohne Marketing-Geschwafel eigenständig und fundiert entscheiden können!', );
Als Letztes müssen wir uns nur noch um die Erstellung von Links für die Seiten untereinander kümmern. Zum Glück für uns enthält das Zend Framework einen URL-Builder in Form der View-Hilfsklasse url(). Listing 15.15 zeigt, wie sie eingesetzt wird.
358
15.3 Eine zweite Sprache für die Places-Applikation Listing 15.15 URLs anhand der View-Hilfsklasse url() erstellen
Wie aus Listing 15.15 zu entnehmen ist, wird das Erstellen eines URLs durch die ViewHilfsklasse url() erleichtert. Das kann noch weiter vereinfacht werden, indem man nicht true als letzten Parameter übergibt, der hier in der Variable $reset gespeichert ist. Wenn $reset false ist (Standard), „merkt“ sich die Hilfsklasse den Zustand aller Parameter, die Sie nicht überschrieben haben. Weil der lang-Parameter nie überschrieben wird, muss er nur angegeben werden, falls der $reset-Parameter auf true gesetzt wird. Wir haben nun anhand von Zend_Translate dafür gesorgt, dass alle Aspekte einer Übersetzung bedacht werden. Weitere Sprachen werden einfach eingefügt, indem die Sprache in config.ini aufgenommen und eine Übersetzungsdatei geschrieben wird. Es hat sich bewährt, einen Mechanismus anzubieten, über den User sich eine gewünschte Sprache für die Site auswählen können. Das läuft häufig über die Landesflagge, weil das in allen Sprachen funktioniert (obwohl es britische Bürger ärgern könnte, dass die USAFlagge fürs Englische steht). Eine Alternative wäre, Text in der jeweiligen Sprache zu verwenden, doch das kann recht schwer in das Design einer Site zu integrieren sein. Um die Konvertierung von Places in eine mehrsprachige Website abzuschließen, auf der sich auch die ausländischen Besucher wie zu Hause fühlen, müssen wir darauf achten, dass auch die dargestellten Datumsangaben mittels Zend_Locale angepasst werden.
15.3.4 Datum mit Zend_Locale korrekt darstellen Wenn Sie sich die Abbildung 15.1 genau anschauen, merken Sie, dass das Datum im falschen Format ist und dass der englische statt des deutschen Monatsnamens erscheint. Daran ist die View-Hilfsklasse displayDate() schuld, die nicht korrekt lokalisiert wurde. Das beheben wir gleich mal. Als Erinnerung kommt hier in Listing 15.16 noch einmal der ursprüngliche Code zur Darstellung des Datums. Listing 15.16 Naive Lokalisierung eines Datums class Zend_View_Helper_displayDate { function displayDate($timestamp, $format='%d %B %Y') { return strftime($format, Gibt das formatierte strtotime($timestamp)); Datum zurück } }
359
15 Internationalisierung und Lokalisierung Wir nehmen in Listing 15.16 strftime(), das dem PHP-Manual zufolge mit Locales umgehen kann. Leider verwendet strftime() das Locale des Servers, wenn Sie nichts anderes angeben, obwohl wir genau wissen, welches Locale gewünscht ist. Dafür gibt es zwei Lösungen: Nehmen Sie setlocale() oder Zend_Date. Auf den ersten Blick ist es sehr verführerisch, setlocale() zu nehmen. Wir müssten in das Front-Controller-Plug-in LanguageSetup einfach nur die folgende Zeile einfügen: setlocale(LC_TIME, $lang);
Allerdings funktioniert das nicht wie erwartet. Das erste Problem mit setlocale() ist, dass es nicht thread-sicher ist. Das bedeutet, dass Sie vor jeder PHP-Funktion, die mit Locales arbeitet, setlocale() aufrufen müssen, wenn Sie mit einem Threaded-Webserver arbeiten. Das zweite Problem besteht darin, dass bei Windows der String, der an die setlocale()-Funktion übergeben wird, nicht der gleiche String ist wie der, den wir in der config.ini-Datei verwenden. Das heißt, für Deutsch wird „de“ verwendet, aber die Windows-Version von setlocale() erwartet „deu“. Als Bestandteil des Zend Frameworks bietet sich Zend_Date an, da es deutlich vorhersagbarer arbeitet. ist eine Klasse, die umfassend alles bearbeitet, was mit der Manipulation und Darstellung von Zeit- und Datumsangaben zusammenhängt. Sie beachtet ebenfalls die Locales, und wenn Sie ihr ein Zend_Locale-Objekt übergeben, wird sie es nutzen, um alle mit Datum und Zeit zusammenhängenden Strings zu übersetzen. Aktuell müssen die Datumsangaben im für die User korrekten Format ausgelesen werden. Also modifizieren wir die View-Hilfsklasse displayDate(), die in views/helpers/DisplayDate.php gespeichert ist (siehe Listing 15.17). Zend_Date
Listing 15.17 Lokalisierung von Datumsangaben anhand von Zend_Date class Zend_View_Helper_displayDate { function displayDate($timestamp, $format = Zend_Date::DATE_LONG) { Liest Locale aus
Das Front-Controller-Plug-in LanguageSetup speicherte das Objekt in die Registry, sodass wir es einfach zur Nutzung mit Zend_Date auslesen können n. Um ein Datum darzustellen, das die Ländereinstellungen beachtet, muss man nur das Zend_Date-Objekt des darzustellenden $timestamps erstellen o und anschließend get() aufrufen p. Die get()Methode akzeptiert einen Parameter, der ein String oder eine Konstante sein kann. Diese Konstanten beachten die Ländereinstellungen, sodass DATE_LONG den Monatsnamen in der richtigen Sprache darstellt. Entsprechend weiß Zend_Date::DATE_SHORT, dass das Format des kurzen Datums in Großbritannien dd/mm/yy und in den USA mm/dd/yy lautet. Im All-
360
15.4 Zusammenfassung gemeinen ist von kurzen Datumsangaben abzuraten, weil es jene User verwirrt, die nicht an Website gewöhnt sind, die Locales beachten. Die deutsche Version von Places inklusive der lokalisierten Datumsangaben sehen Sie in Abbildung 15.2. Außerdem kann der Abbildung entnommen werden, dass der von Zend_Date::DATE_LONG für die deutschen User erstellte Datums-String so ist wie von ihnen erwartet. Also erkennen unsere deutschen Gäste, dass sie auf dieser Site Bürger erster Klasse sind und nicht erst als nachträglicher Einfall berücksichtigt wurden.
Abbildung 15.2 Durch Einsatz von Zend_Date, das auf Locales achtet, können wir das Datum in der korrekten Sprache darstellen.
15.4 Zusammenfassung Mit Zend_Locale und Zend_Translate können Websites deutlich einfacher unter Berücksichtigung der jeweiligen Ländereinstellungen in mehreren Sprachen erstellt werden. Natürlich ist es nicht wirklich einfach, eine Website in verschiedenen Sprachen anzubieten, weil man z. B. darauf achten muss, dass der Text in den bereitgestellten Platz passt, und Sie auch Übersetzungen brauchen, die funktionieren! ist der Kern der Lokalisierung im Zend Framework. Damit können in verschiedenen Sprachen geschriebene Zahlen- und Datumsangaben normalisiert werden, damit sie einheitlich gespeichert und in der Applikation verwendet werden können.
Zend_Locale
361
15 Internationalisierung und Lokalisierung übersetzt Text anhand der _()-Methode in eine andere Sprache. Es unterstützt mehrere Eingabeformate, insbesondere auch das weitverbreitete gettext()-Format, damit Sie für Ihr Projekt das passende Format finden. Kleinere Projekte werden mit Arrayund CSV-Formaten arbeiten, während bei größeren Projekten wahrscheinlich gettext, TBX, Qt, XLIFF und XmlTm zum Einsatz kommen. Durch die Flexibilität des Adaptersystems von Zend_Translate wird die Hauptcodebasis durch die Migration von einem einfacheren zu einem robusteren System überhaupt nicht betroffen. Zend_Translate
Damit sind Internationalisierung und Lokalisierung von Applikationen abgedeckt, und wir können uns um eine andere Sorte der Übersetzung kümmern: Ausgabeformate. Obwohl alle Websites gedruckt werden können, ist es manchmal einfacher und besser, eine PDFVersion einer Seite anzubieten. Mit Zend_Pdf können PDF-Dokumente ohne viele Umstände erstellt und bearbeitet werden – das ist das Thema des nächsten Kapitels.
362
16 16 PDFs erstellen Die Themen dieses Kapitels
PDFs mit Zend_PDF erstellen, speichern und laden Text und Formen auf einer Seite zeichnen Farben und Stilvorlagen einfügen Objekte drehen und beschneiden Einen PDF-Berichtsgenerator erstellen So überraschend es uns erscheinen mag, die wir uns ungesund lange in der digitalen Welt herumtreiben, aber es gibt immer noch eine ganze Menge Leute, die Dokumente in Papierformat brauchen und damit arbeiten. Obwohl durch klugen Einsatz von HTML und CSS gut formatierte Webseiten produziert werden, gibt es immer noch Grenzen, wie pixelgenau Webseiten sein können, vor allem, was den Ausdruck angeht. Von Adobe Systems stammt das PDF (Portable Document Format), um die Lücke zwischen gedruckten und digitalen Dokumenten zu schließen. Seitdem ist PDF zum Standard für webbasierte, ausdruckbare Dokumente geworden und überdies ein integraler Bestandteil eines modernen Grafik-Workflows. Die Präzision des PDF-Formats ist besonders wichtig für Dokumente wie speziell formatierte Kopien mit Inhalten aus Webseiten, als EMail versandte Rechnungen, Website-Statistiken und andere Berichte. In diesem Kapitel generieren wir mit der Zend_Pdf-Komponente des Zend Frameworks einen Beispielbericht und erläutern dabei verschiedene Features. Doch vorher sollten wir die Komponente gründlich vorstellen.
363
16 PDFs erstellen
16.1 Die Grundlagen von Zend_Pdf Nur mit Verwendung von PHP erlaubt Zend_Pdf das Erstellen, Laden und Speichern von PDF-Dokumenten (Version 1.4) und stellt Befehle für Texte, das Zeichnen von Formen und Bilder zur Verfügung. Die Anzahl der Klassen, aus denen diese Komponente besteht, lässt darauf schließen, dass deren Erstellung mit viel Arbeit verbunden war. Und doch fehlen aktuell noch ein paar Funktionalitäten, die man erwarten sollte. Man ist beispielsweise gezwungen, Zeilenumbruch und Paginierung manuell oder über Workarounds zu erledigen. Auch die Dokumentation ist immer noch etwas spärlich. Positiv zu nennen ist, dass man sich bereits dahin aufgemacht hat, solche angefragten Features hinzuzufügen. Also können Sie davon ausgehen, dass hier investierte Arbeit nicht vergebens ist. Nach dieser Vorbemerkung schauen wir uns an, wie PDF-Dokumente erstellt oder geladen werden.
16.1.1 Erstellen oder Laden von Dokumenten bietet verschiedene Möglichkeiten, um PDF-Dokumente zu erstellen und zu laden. Um ein erstes PDF-Dokument zu erstellen, instanziiert man wie folgt ein neues Zend_Pdf-Objekt: Zend_Pdf
$pdf = new Zend_Pdf();
Es gibt zwei Möglichkeiten, ein vorhandenes PDF-Dokument zu laden; beide arbeiten mit einer statischen Methode. Die erste lädt aus einer Datei: $file = '/Pfad/zum/beispiel.pdf'; $pdf = Zend_Pdf::load($file);
Die zweite lädt aus einem String, der den Inhalt eines PDF-Dokuments enthält: $pdf = Zend_Pdf::parse($pdfString);
Egal ob Sie das PDF-Dokument erstellt oder geladen haben, Sie arbeiten damit auf die gleiche Weise. Jedes Dokument besteht aus Seiten, auf denen Sie Texte, Bilder oder Formen zeichnen.
16.1.2 Seiten im PDF-Dokument erstellen Wenn Sie Ihr Zend_Pdf-Objekt erstellt haben, können Sie mit den Seiten Ihres Dokuments arbeiten, als hätten Sie es mit einem regulären PDF-Array zu tun. Das pages-Array ist $pdf->pages, und alle normalen PHP-Array-Funktionen funktionieren wie gewohnt. Für eine neue Seite erstellen Sie ein Zend_Pdf_Page-Objekt und setzen dessen Seitengröße wie folgt: $page = new Zend_Pdf_Page(Zend_Pdf_Page::SIZE_A4);
364
16.1 Die Grundlagen von Zend_Pdf Das hier verwendete Zend_Pdf_Page::SIZE_A4-Argument arbeitet mit einer Konstante, um zu definieren, welche Größe und Ausrichtung die Seite haben soll. Tabelle 16.1 zeigt die verfügbaren vordefinierten Konstanten zusammen mit den Größenangaben. Tabelle 16.1 Vordefinierte Zend_Pdf_Page-Konstanten für die Seitengröße mit Maßangaben für Breite und Höhe Konstante
Größe in Zoll (Inch)
Größe in Millimeter
Größe in Punkt (1/72 Zoll)
SIZE_A4
8,27 x 11,69
210 x 297
595 x 842
SIZE_A4_LANDSCAPE
11,69 x 8,27
297 x 210
842 x 595
SIZE_LETTER
8,5 x 11
215,9 x 279,4
612 x 792
SIZE_LETTER_LANDSCAPE
11 x 8,5
279,4 x 215,9
792 x 612
Sie sind nicht auf vordefinierte Größen begrenzt. Jede Seite kann ihre eigene Größe und Ausrichtung haben. Somit können Sie beliebige Werte für Breite und Höhe wählen und die Argumente wie folgt anordnen: $page = new Zend_Pdf_Page($width, $height);
Vielleicht arbeiten Sie lieber mit Zoll oder Millimeter, doch die hier eingegebenen Werte werden als Punkt behandelt. Also müssen Sie sie konvertieren. Anmerkung
Alle Maßangaben in Zend_Pdf sind in Punkt. Wenn Sie lieber mit Zoll oder Millimeter arbeiten, sollten Sie die in Punkt konvertieren: $inches * 72 oder $millimeters / 25.4 * 72.
Wenn Sie sich beim Erstellen der Seite für die statische Methode Zend_Pdf::newPage() entschieden haben, wird diese Seite dann schon ans Dokument angehängt. Im Kontrast dazu sind direkt instanziierte Zend_Pdf_Page-Objekte unabhängig und müssen dem Dokument wie folgt hinzugefügt werden: $pdf->pages[] = $page;
Nun können wir das Dokument einrichten und Seiten einfügen, aber diese werden dann leer sein. Bevor wir sie mit Inhalt füllen, sollten wir in den Metainformationen ein paar Informationen über das Dokument selbst einfügen.
16.1.3 Metainformationen im Dokument einfügen Ein Vorteil von digitalen gegenüber Dokumenten aus Papier ist, wie einfach sie basierend auf Dateiinhalt und Dokumentinformation zu verwalten sind. Diese Metainformationen für das Dokument kann man einflechten, indem man Werte im properties-Array des
365
16 PDFs erstellen Zend_Pdf-Objekts
setzt. Der Titel des Dokuments wird beispielsweise wie folgt angege-
ben: $pdf->properties['Title'] = 'Zend_Pdf macht tolle PDFs';
Natürlich gibt es noch viel mehr Eigenschaften als nur den Titel. In Tabelle 16.2 sehen Sie die in PDF v1.4 verfügbaren Schlüssel, mit denen Zend_Pdf arbeiten kann. Tabelle 16.2 Die Schlüssel für die Metainformationen, die für Zend_Pdf in PDF v1.4-Dokumenten verfügbar sind Name
Typ
Beschreibung
Title
String
Der Titel des Dokuments
Author
String
Der Name des Erstellers des Dokuments
Subject
String
Das Thema des Dokuments
Keywords
String
Mit dem Dokument verknüpfte Schlüsselwörter
Creator
String
Die Applikation, mit der das Originaldokument vor der Konvertierung erstellt wurde, falls das Dokument aus einem anderen Format in PDF konvertiert wurde.
Producer
String
Die Applikation, die die Konvertierung durchgeführt hat, falls das Dokument aus einem anderen Format in PDF konvertiert wurde.
CreationDate
String
Datum und Zeit der Erstellung des Dokuments. Wird über Zend_Pdf::pdfDate() befüllt, damit das Datum korrekt formatiert wird.
ModDate
String
Datum und Zeit der letzten Änderung des Dokuments. Wird über Zend_Pdf::pdfDate() befüllt, damit das Datum korrekt formatiert wird.
Trapped
Boolean
Zeigt, ob das Dokument modifiziert wurde, um eingeschlossene Informationen zu enthalten. Wenn das der Fall ist, ist der Wert true, ansonsten false. Hierdurch werden Ausrichtungsfehler beim Drucken behoben, indem benachbarte Farben einander überlappen, ist aber nur für bestimmte Druckvorgänge relevant.
Wenn man das generierte Dokument in einem PDF-Reader öffnet und sich die Dokumenteigenschaften anschaut, kann man schnell prüfen, ob die Metainformationen korrekt gesetzt wurden. Anmerkung
Weil Zend_Pdf über das properties-Array Metainformationen aus PDF-Dokumenten lesen kann, kann es selbst auch in einer Situation eingesetzt werden, wo das Datenmanagement erforderlich ist, z. B. wenn die Katalogisierung von PDF-Dokumenten erforderlich ist.
366
16.2 Einen PDF-Berichtsgenerator erstellen Jetzt können wir ein Dokument mit leeren Seiten erstellen, das Metainformationen enthält. Das hört sich nicht sonderlich nützlich an, reicht aber zum Speichern oder Ausgeben.
16.1.4 Speichern des PDF-Dokuments Nachdem Sie die Arbeit mit dem PDF-Dokument abgeschlossen haben, können Sie es wie folgt anhand der Zend_Pdf::save()-Methode in eine Datei speichern: $file = '/Pfad/zum/tollen/neuen/beispiel.pdf'; $pdf->save($file);
Oder Sie lassen die Zend_Pdf::render()-Methode das PDF-Dokument als String zurückgeben: $pdfString = $pdf->render();
Der resultierende String könnte dann beispielsweise in eine Datei oder Datenbank gespeichert, in ein Zip-Archiv eingefügt, an eine E-Mail angehängt oder im Browser mit dem korrekten MIME-Header ausgegeben werden. Das reicht bereits, um uns gleich an die erste Komponente des Beispielberichts machen zu können.
16.2 Einen PDF-Berichtsgenerator erstellen Wir nehmen einmal an, dass Places to take the kids! ans Netz gegangen und ausreichend lange gelaufen ist, um ein paar statistische Werte über die Nutzung abschöpfen zu können. Nach verschiedenen Meetings, bei denen wir über Weiterentwicklung und zukünftige Ausrichtung der Site nur anhand verschiedener vager Informationen sprechen konnten, beschließen wir, dass wir mit Zend_Pdf einen fundierten Bericht produzieren können. Wir brauchen eine einzelne Seite mit einer Einführung und einem einfachen Jahresdiagramm der Performance von jeder Funktionalität der Site. Das soll in den Meetings als Entscheidungshilfe genutzt werden, also brauchen wir noch etwas Platz am Rand für Notizen.
16.2.1 Das Model für das Berichtsdokument Der Code zum Generieren des Berichts (siehe Listing 16.1) beginnt mit einer ModelKlasse für das Dokument selbst. Wir nennen die Klasse Report_Document und speichern sie den Namenskonventionen des Zend Frameworks gemäß in der Datei application/models/Report/Document.php, wobei der Klassenname auf die Verzeichnisstruktur abgebildet wird. Durch Erstellung dieser Klasse können wir ein paar Standard-Features setzen, z. B. die Metainformationen des Dokuments, und bekommen ein aufgeräumtes Interface, mit dem später die Berichte erstellt werden können.
367
16 PDFs erstellen Listing 16.1 Die Model-Klasse des Berichtsdokuments bildet die Basis für den Berichtsgenerator. class Report_Document { protected $_pdf; public function __construct() { $this->_pdf = new Zend_Pdf(); $this->_pdf->properties['Title'] = 'Yearly Statistics Report'; $this->_pdf->properties['Author'] = 'Places to take the kids'; }
Erstellt neues Zend_Pdf-Objekt Setzt Metainformationen des Dokuments
public function addPage(Report_Page $page) { $this->_pdf->pages[] = $page->render(); }
Fügt Seitenobjekt in Dokument ein
public function getDocument() { return $this->_pdf; } }
Wir hätten Zend_Pdf auch erweitern können, um diese Klasse zu erstellen, aber damit das Beispiel klar bleibt, favorisieren wir Komposition vor Vererbung (Composition over inheritance) und machen aus $_pdf (eine Instanz von Zend_Pdf) eine Eigenschaft. Die grundlegende Funktion von Report_Document ist, ein PDF-Dokument mit Metainformationen einzurichten und dann nur Seiten zu erlauben, die Instanzen jener Report_Page-Klasse sind, der wir uns gleich zuwenden.
16.2.2 Das Model für die Berichtsseite Im restlichen Verlauf dieses Kapitels werden wir unsere Berichtsseite erstellen, indem wir den Inhalt einfügen und dabei auch gleichzeitig die verschiedenen Arten vorstellen, wie diese Inhalte mit Zend_Pdf eingefügt werden können. Doch vorher machen wir anhand des anfänglichen Setups unserer Report_Page-Klasse (siehe Listing 16.2) ein paar Anmerkungen über die Arbeit mit Zend_Pdf. Auch hier gilt: Wegen des Unterstrichs im Klassennamen gehört diese Klasse ins Verzeichnis application/models/Report. Listing 16.2 Der anfängliche Setup-Code für die Model-Klasse Report_Page class Report_Page { protected $_page; protected $_yPosition; protected $_leftMargin; protected $_pageWidth; protected $_pageHeight; protected $_normalFont;
368
16.2 Einen PDF-Berichtsgenerator erstellen protected protected protected protected protected
public function __construct() der vertikalen Position { $this->_page = new Zend_Pdf_Page( Setzt linken Zend_Pdf_Page::SIZE_A4); Rand $this->_yPosition = 60; $this->_leftMargin = 50; $this->_pageHeight = $this->_page->getHeight(); Holt Höhe und $this->_pageWidth = $this->_page->getWidth(); Breite der Seite $this->_normalFont = Zend_Pdf_Font::fontWithName( Auswahl der Zend_Pdf_Font::FONT_HELVETICA); Standard-Fonts $this->_boldFont = Zend_Pdf_Font::fontWithName( Zend_Pdf_Font::FONT_HELVETICA_BOLD); }
}
Aus weitgehend den gleichen Gründen wie beim Dokument-Model für unseren Bericht haben wir beschlossen, für diese Klasse Zend_Pdf_Page nicht zu erweitern, sondern sie stattdessen als Eigenschaft zu verwenden n. Als Erstes müssen wir mit der Tatsache zurechtkommen, dass wie beim PDF-Standard alle Zeichenvorgänge in Zend_Pdf von der unteren linken Ecke der Seite ausgehen. Damit müssen erst einmal all jene klarkommen, die an Desktop-Applikationen gewöhnt sind, die von oben links nach unten arbeiten. Deswegen haben wir nun beschlossen, mit einer Art Schiebemarker auf der y-Achse zu arbeiten, der anfänglich auf 60 Punkte nach oben vom unteren Seitenrand aus gesetzt ist o. Entsprechend haben wir einen linken Rand auf der x-Achse in der Position 50 Punkte von der linken Seitenrand gesetzt p. Warum wir diese Einstellungen vorgenommen haben, wird gleich deutlicher, wenn wir mit dem Zeichnen von Elementen auf der Seite beginnen. Nach dem Setzen dieser Referenzpunkte auf der y-Position und dem linken Rand müssen wir wissen, wie groß die Seite ist, was anhand der Methoden Zend_Pdf_Page::get Height() und Zend_Pdf_Page::getWidth()bestimmt wird q. Weil wir mit einer A4-Seite arbeiten, werden die Punktwerte 595 bzw. 842 zurückgegeben. Wenn wir die Seitengröße auf z. B. Size_Letter ändern wollten, könnten wir das Layout mit diesen Einstellungen nach Bedarf anpassen. Die abschließenden Einstellungen im Konstruktor sind ein paar Standard-Fonts r, die uns als gute Überleitung zum ersten Zeichen-Feature von Zend_Pdf dienen: dem Einfügen von Text.
369
16 PDFs erstellen
16.3 Text auf einer Seite zeichnen Text wird als einzelne, nicht umbrochene Zeile entlang einer Grundlinie gezeichnet, die von einer x- und y-Position auf der Seite ausgeht. Vor dem Einfügen von Text muss auf jeden Fall erst einmal ein Font gesetzt werden. Dafür kann einer der 14 Standard-Fonts von PDF oder ein selbst erstellter genommen werden.
16.3.1 Die Wahl der Fonts Bei der Report_Page-Klasse in Listing 16.2 haben wir die Fonts Helvetica und Helvetica bold über den Namen angegeben, und zwar mit Zend_Pdf_Font::fontWithName() und der relevanten Zend_Pdf_Font-Konstante. Wenn wir uns an die Standard-Fonts halten wollen, können wir nur eine der folgenden Optionen nehmen:
Alternativ könnten wir mit einer Anweisung wie der folgenden aus einer Datei auch einen TrueType- oder OpenType-Font laden: $font = Zend_Pdf_Font::fontWithPath('arial.ttf');
Beachten Sie, dass eigene Fonts standardmäßig ins PDF-Dokument eingebettet werden. Wenn Ihnen besonders daran gelegen ist, dass die Datei klein bleibt, und Sie sicher sind, dass der Adressat die Fonts auf seinem System hat, können Sie als das optionale zweite Argument für fontWithPath()mit der Zend_Pdf_Font::EMBED_DONT_EMBED-Konstante arbeiten. Wenn das Font-Objekt fertig ist, ist es auf der Seite einsetzbar.
16.3.2 Den Font setzen und Text einfügen Unsere Berichtsseite braucht als Erstes eine Kopfzeile, und in Listing 16.3 haben wir eine setHeader()-Methode erstellt, die einen einfachen Titel mit einer horizontalen Linie darunter produziert.
370
16.3 Text auf einer Seite zeichnen Listing 16.3 Die setHeader()-Methode der Report_Page-Klasse
public function setHeader() Speichert { Grafikstatus Setzt $this->_page->saveGS(); Font $this->_page->setFont($this->_boldFont, 20); $this->_page->drawText($this->_headTitle, Zeichnet $this->_leftMargin, Titeltext $this->_pageHeight - 50); $this->_page->drawLine($this->_leftMargin, Zeichnet hori$this->_pageHeight - 60, zontale Linie $this->_pageWidth - $this->_leftMargin, $this->_pageHeight - 60); $this->_page->restoreGS(); }
Stellt Grafikstatus wieder her
Nach dem Setzen des Fonts für den Titel (Größe 20 Punkt und in Fett) anhand von Zend_Pdf_Page::setFont() o können wir das auf der Seite zeichnen. Das erste Argument für Zend_Pdf_Page::drawText() ist der Text, den Sie auf der Seite platzieren wollen, gefolgt von den Punkten für von x und y, für die wir den linken Rand und eine Position von 50 Punkt unterhalb des oberen Seitenrands verwendet haben p. Weitere 10 Punkt unter diesem Titeltext fügen wir dann eine horizontale Linie vom linken bis zum rechten Rand ein q. Ihnen wird aufgefallen sein, dass wir vor der Arbeit am Text Zend_Pdf_Page::saveGS() n aufgerufen haben und mit dem Aufruf von Zend_Pdf_Page::restoreGS() enden r. Der Grund dafür ist, dass alle Änderungen, die wir vorher an den Dokumenteinstellungen vorgenommen haben, auf diese Methode beschränkt bleiben, sodass wir auf diese Dokumenteinstellungen zurückgreifen wollen, wenn wir fertig sind. Anmerkung Zend_Pdf_Page arbeitet in einer Weise, die uns an den Ausgabepuffer von PHP erinnert,
und kann isolierte Änderungen an Stilvorlagen vornehmen, indem man zuerst den aktuellen Grafikzustand anhand von saveGS()speichert und dann über restoreGS() dahin zurückkehrt, falls irgendwelche isolierten Änderungen vorgenommen wurden.
Da wir uns gerade mit dem Einfügen von Text beschäftigen, erinnern Sie sich vielleicht noch daran, dass eine der Anforderungen für den Beispielbericht war, dass auf jeder Seite ein Einführungstext über der Information stehen sollte. Weil es sehr wahrscheinlich ist, das diese Einführung mehr als nur eine Textzeile enthält, müssen wir für den Textumbruch einen kleinen Workaround einsetzen.
16.3.3 Umbrochenen Text einfügen Text wird als einzelne, nicht umbrochene Zeile gezeichnet, was bedeutet, dass sie aus der Seite läuft, wenn die Zeile länger als die Seitenbreite ist. Um das zu verhindern, erstellen wir eine wrapText()-Methode (siehe Listing 16.4).
371
16 PDFs erstellen Listing 16.4 Die wrapText()-Methode, mit der umbrochener Text gezeichnet werden kann
Bricht Text bei anprotected function wrapText($text) gegebener Breite um { $wrappedText = wordwrap($text, 110, "\n", false); $token = strtok($wrappedText, "\n"); $this->_yPosition = $this->_pageHeight - 80;
Teilt um-
brochenen Text
Gibt y-Start Schleife durchläuft jeden in position an
Tokens aufgeteilten String while ($token !== false) { $this->_page->drawText($token, Zeichnet in $this->_leftMargin, Tokens aufgeteilten String $this->_yPosition); $token = strtok("\n"); $this->_yPosition -= 15; Teilt String in }
Verschiebt y-Position
}
Tokens auf
15 Punkt nach unten
Die wrapText()-Methode verwendet zuerst die wordwrap-Funktion von PHP, um den angegebenen Text auf eine Breite zu bringen, die in die Berichtsseite passt, und umbricht ihn anhand des Zeilenvorschub-Delimiters n. Mit diesem Delimiter wird der Text dann in kleinere Strings aufgeteilt o, durch die dann eine Schleife läuft p. Jedes String-Segment wird dann am linken Randpunkt und der aktuellen y-Position auf die Seite gezeichnet q, die wir anfänglich auf 80 Punkt vom oberen Seitenrand nach unten gesetzt haben r. Der verbleibende String wird erneut geteilt (denken Sie daran, dass mit strtok() nur der erste Aufruf ein String-Argument braucht) s, und die y-Position wird um 15 Punkt nach unten verschoben, damit die nächste Textzeile gezeichnet werden kann t. Den einführenden Text brauchen wir nun aus der Report_Page-Klasse heraus einfach nur noch an die wrapText()-Methode zu übergeben: $this->wrapText($this->_introText);
Natürlich ist diese Methode nicht die einzige, um Text zu umbrechen, reicht aber für die Report_Page-Klasse, und wir gehen davon aus, dass für Zend_Pdf in zukünftigen Erweiterungen eigene Lösungen entwickelt werden. In Abbildung 16.1 sehen Sie das Ergebnis unserer bisherigen Bemühungen.
Places reviews This is the report for reviews. Reviews are very important to Places to take the kids because they are an indication not only of how many people are reading the places information but of how confident users are in the community element of the site.
Abbildung 16.1 Der generierte Header und der Einführungstext des PDF-Berichts
Unterhalb des Titels zeichnen wir eine horizontale Linie (, die gleich noch Thema sein wird. Doch vorher müssen noch weitere Angaben gemacht werden, und wir beginnen mit den Farben.
372
16.4 Die Arbeit mit Farben
16.4 Die Arbeit mit Farben Neben der Grauskala und den Farbräumen RGB und CMYK unterstützt Zend_Pdf auch die Verwendung von HTML-Farben, was für Webentwickler eine praktische Ergänzung ist. Wir können Farben im PDF-Dokument verwenden, indem wir ein Zend_Pdf_Color-Objekt erstellen, das aus einem dieser vier Farbräume ausgewählt wird.
16.4.1 Die Wahl der Farben Weil die Methoden zur Farbeinstellung in Zend_Pdf_Page alle ein Zend_Pdf_Color-Objekt als Argument erwarten, müssen wir zunächst einmal auch eins auswählen und erstellen. Ein Grauskala-Objekt wird mit einem Argument erstellt, das einen Wert zwischen 0 (schwarz) und 1 (weiß) enthält: $grayLevel = 0.5; $color = new Zend_Pdf_Color_GrayScale ($grayLevel);
Ein RGB-Objekt akzeptiert die drei Argumente Rot, Grün und Blau mit Werten auf einer Intensitätsskala von 0 (Minimum) bis 1 (Maximum): $red = 0; $green = 0.5; $blue = 1; $color = new Zend_Pdf_Color_Rgb ($red, $green, $blue);
Ein CMYK-Objekt akzeptiert basierend ebenfalls auf einer Intensitätsskala von 0 (Minimum) bis 1 (Maximum) die vier Argumente Cyan, Magenta, Yellow und Black: $cyan = 0; $magenta = 0.5; $yellow = 1; $black = 0; $color = new Zend_Pdf_Color_Cmyk ($cyan, $magenta, $yellow, $black);
Ein HTML-Farbobjekt akzeptiert die üblichen Hex-Werte oder HTML-Farbnamen: $color = new Zend_Pdf_Color_Html('#333333'); $color = new Zend_Pdf_Color_Html('silver');
Nach Erstellen eines Zend_Pdf_Color-Objekts können wir damit fortfahren, Objekte für den Einsatz einzurichten.
16.4.2 Farben einstellen Wie Sie sich sicher denken können, gibt es zwei übliche Wege, um für Zeichenobjekte Farben zu setzen: durch Ausfüllen und durch Striche oder Linien. Die Füllfarbe wird gesetzt, indem ein Farbobjekt als Argument übergeben wird: $page->setFillColor($color);
373
16 PDFs erstellen Die Linienfarbe wird auf die gleiche Weise gesetzt: $page->setLineColor($color);
Zeileneinstellungen werden nicht nur auf gezeichnete Linien angewendet, sondern auch auf die Striche (Umrisse) von Formen. Füllfarben können sowohl auf Text als auch auf Formen angewendet werden. Nach der Erläuterung von Fonts und Farben organisieren wir diese Einstellungen im nächsten Abschnitt über Stilvorlagen.
16.5 Die Arbeit mit Styles Durch Zend_Pdf_Style werden solche Einstellungen wie Füll- oder Strichfarbe, Strichbreite und Fonts in Stilvorlagen (Styles) kombiniert, die als Gruppe auf die ganze Seite angewandt werden. So sparen wir Programmzeilen und können umfassendere Änderungen an den Styles leichter vornehmen. In Listing 16.5 sehen Sie, dass wir in der Report_Page-Klasse eine setStyle()-Methode erstellt haben, aus dem ein Style-Objekt für die Berichtsseiten geschaffen wird. Wenn unser Berichtsgenerator komplizierter wird, können wir diese Methode bei Bedarf leicht in eine eigene Klasse refaktorieren. Listing 16.5 Einen Satz Styles für die Klasse Report_Page erstellen public function setStyle() Erstellt { Style-Objekt $style = new Zend_Pdf_Style(); $style->setFont(Zend_Pdf_Font::fontWithName( Setzt Zend_Pdf_Font::FONT_HELVETICA), 10); $style->setFillColor( Standard-Styles new Zend_Pdf_Color_Html('#333333')); $style->setLineColor( new Zend_Pdf_Color_Html('#990033')); Fügt Style in $style->setLineWidth(1); page-Objekt ein $this->_page->setStyle($style); }
Abgesehen von der Einstellung der Zeilenbreite sollten diese Settings nun recht vertraut sein, außer dass sie auf das Style-Objekt angewandt wurden, das dann als Argument an Zend_Pdf_Page::setStyle() übergeben wird. Wenn sie gesetzt sind, sind diese Einstellungen im Dokument der Standard, falls sie nicht überschrieben werden. Styles werden nur auf Elemente angewandt, die nach dem Setzen der Styles gezeichnet werden, und nicht auf bereits gezeichnete Elemente. Die Berichtsseite hat nun schon eine Menge Einstellungen, aber nur recht wenig Inhalt, also wird es Zeit, dieses zu ändern. Darum kümmern wir uns jetzt und zeichnen die benötigten Diagramme anhand von Formen.
374
16.6 Formen zeichnen
16.6 Formen zeichnen Mit Zend_Pdf können wir Linien, Rechtecke, Polygone, Kreise und Ellipsen zeichnen. Doch vorher sollten wir ein paar Einstellungen wie die Farbe (in Abschnitt 16.4 erläutert) oder die Linienbreite (siehe Listing 16.5) definieren: $style->setLineWidth(1);
Die Linienbreite wird hier wie alle Maßangaben in Zend_Pdf in Punkt angegeben. Da es hier gerade um die Linienbreite geht, sollten wir auch erklären, wie man diese horizontale Linie unter dem Seitentitel ausgibt. Schauen wir uns an, wie das geht.
16.6.1 Linien zeichnen In der setHeader()-Methode in Listing 16.3 haben wir anhand des folgenden Befehls eine Linie unter den Seitentitel gezeichnet: $this->_page->drawLine($this->_leftMargin, $this->_pageHeight - 60, $this->_pageWidth - $this->_leftMargin, $this->_pageHeight - 60);
In dieser Funktion geben wir zwei Punkte mit den x- und y-Positionen an, und dazwischen wird dann ein Strich gezogen. Wir haben von daher die fixen und relativen Werte in der Berichtsklasse verwendet, um den ersten Punkt auf 50,782 und den zweiten auf 545,782 zu setzen – also wird daraus ein einfacher horizontaler Strich. Da keine anderen Angaben gemacht werden, wird daraus also ein durchgezogener Strich, aber es hätte auch eine gestrichelte Linie sein können.
16.6.2 Gestrichelte Linien setzen Eine weitere Anforderung der Berichtsseite war, einen Abschnitt für Notizen beim Meeting zu haben. Lassen wir mal alle persönlichen Vorlieben für liniertes oder Blankopapier beiseite und fügen eine Reihe blasser gestrichelter Linien in den Notizenabschnitt ein. In Listing 16.6 sehen Sie, wie diese Einstellung in der getNotesSection()-Methode erscheint und wie der Grafikstatus genutzt wird, um an den Strichen temporäre Änderungen vorzunehmen.
375
16 PDFs erstellen Listing 16.6 Gestrichelte Linien in einem Bereich für Notizen protected function getNotesSection() { $this->_yPosition -= 20; $this->_page->drawText('Meeting Notes', $this->_leftMargin, $this->_yPosition); $this->_yPosition -= 10; $this->_page->drawLine($this->_leftMargin, $this->_yPosition, $this->_pageWidth $this->_leftMargin, $this->_yPosition); $noteLineHeight = 30; $this->_yPosition -= $noteLineHeight;
Zeichnet Überschrift für Abschnitt
Setzt Höhe der Notizzeilen Speichert aktuellen
Grafikstatus $this->_page->saveGS(); $this->_page->setLineColor( Setzt new Zend_Pdf_Color_Html('#999999')); Linien-Style $this->_page->setLineWidth(0.5); $this->_page->setLineDashingPattern(array(2, 2)); while($this->_yPosition > 70) { Zeichnet $this->_page->drawLine($this->_leftMargin, Zeilen in $this->_yPosition, einer Schleife $this->_pageWidth $this->_leftMargin, $this->_yPosition); $this->_yPosition -= $noteLineHeight; Stellt Grafikstatus } wieder her $this->_page->restoreGS();
Setzt Muster für gestrichelte Linie
}
Das erste Argument für Zend_Pdf_Page::setLineDashingPattern() ist ein Array mit der Länge der aufeinanderfolgenden Striche und Zwischenräume, wobei die erste Zahl für den kurzen Strich und die zweite für den Zwischenraum steht. Diese Strich-ZwischenraumSequenz kann so oft wie nötig wiederholt werden. Die in Listing 16.6 verwendete Einstellung ist ein simpler 2-Punkt-Strich, gefolgt von einem 2-Punkt-Zwischenraum (siehe Abbildung 16.2).
Meeting Notes
Abbildung 16.2 Der Abschnitt für die Notizen zeigt, wie Zeilen und gestrichelten Linien eingesetzt werden.
376
16.6 Formen zeichnen Nachdem wir mit gestrichelten Linien einen Notizbereich gezeichnet haben, sind wir de facto zu weit vorgesprungen und müssen für das Zeichnen des Diagramms nun einen Schritt zurück machen. Dafür schauen wir uns an, wie man rechteckige Formen zeichnet.
16.6.3 Rechtecke und Polygone zeichnen Rechteckige Formen werden mit vielen der gleichen Argumente gezeichnet wie die Striche. Die ersten bei Argumente beziehen sich auf die x- und y-Positionen der unteren linken Ecke des Rechtecks, und die nächsten beiden Argumente legen die x- und y-Positionen der oberen rechten Ecke fest: $page->drawRectangle($x1, $y1, $x2, $y2, $fuelltyp);
Zum Zeichnen eines Polygons verwenden Sie den folgenden Befehl: $page->drawPolygon( array($x1, $x2, ..., $xn), array($y1, $y2, ..., $yn), $fuelltyp, $fuellmethode );
Das erste Argument ist ein Array aller x-Werte in der richtigen Reihenfolge, während das zweite ein Array der entsprechenden y-Werte ist. Mit dem dritten Argument wird die Art der Füllung und der Linie definiert, und da haben wir drei Optionen:
Zend_Pdf_Page::SHAPE_DRAW_FILL_AND_STROKE: Damit werden den aktuellen Einstellungen der Seite entsprechend sowohl die Füllfarbe als auch die Linie gezeichnet.
Zend_Pdf_Page::SHAPE_DRAW_FILL: Damit wird nur die Füllung gezeichnet und die Linie ausgelassen.
Zend_Pdf_Page::SHAPE_DRAW_STROKE: Damit wird nur die Linie gezeichnet, was also auf einen Umriss der Form hinausläuft. Das vierte Argument ist so komplex, dass dessen Beschreibung eine eigene Seite füllen würde. Es ist auch recht unwahrscheinlich, dass Sie es brauchen, außer Sie wollen beispielsweise, dass eine Form eine andere überdecken soll. Für einen solchen Fall gehen Sie bitte zur PDF-Spezifikation unter http://www.adobe.com/devnet/pdf/pdf_reference.html, wo Sie nach den Optionen „Nonzero winding number rule“ und „Even-odd rule“ recherchieren, die in Zend_Pdf_Page wie folgt eingesetzt werden können: Zend_Pdf_Page::FILL_METHOD_EVEN_ODD Zend_Pdf_Page::FILL_METHOD_NON_ZERO_WINDING
Für unsere Berichte werden wir mit der drawRectangle()-Methode arbeiten, um ein Balkendiagramm zu zeichnen, bei dem jedes Rechteck die Daten von einem Monat darstellt. Listing 16.7 zeigt die getGraphSection()-Methode, die das Diagramm produziert.
377
16 PDFs erstellen Listing 16.7 Die getGraphSection()-Methode der Report_Page-Klasse
Bewegt x-Position, um nächste Spalte zu zeichnen Schafft Platz unter Diagramm
16.6 Formen zeichnen Die getGraphSection()-Methode beginnt mit dem Speichern des aktuellen Grafikstatus n und fügt eine Kopfzeile für das Diagramm ein o. Dann werden die Anfangspositionen von x und y gesetzt, wobei die y-Position auf dem aktuellen Marker und dem Maximalwert der Diagrammdaten beruht p, gefolgt von der Spaltenbreite q. Für die Schleife über die Daten r nehmen wir den Wert $key, damit die Farbeinstellungen für die Spalten sich abwechseln s. Jeder Wert einer Spalte wird oben gezeichnet t und dann ein Rechteck für die Spalte selbst gezeichnet, bei der wir angeben, dass sie keine Linie haben soll u. Nach Setzen des Markers für die y-Position auf 20 Punkt unter der Spalte v bekommen wir den Kurztext für den Monat mit Zend_Date und können ihn zeichnen w. Die erste Spalte ist nun fertig, und wir schieben die x-Position an der Spaltenbreite entlang, wo dann gleich die nächste Spalte gezeichnet wird . Nachdem alle Monate gezeichnet wurden, fügen wir unter dem Diagrammbereich noch etwas Platz ein, indem wir den Marker für die y-Position auf 20 Punkt tiefer setzen , und stellen dann den Grafikstatus wieder her . Bei Aufruf produziert diese Methode den in Abbildung 16.3 gezeigten endgültigen Output. 90
80
80
60 45 30
30
25 10
Jan
Feb Mar Apr May Jun
Jul
20
Aug Sep Oct
0 0 Nov Dec
Abbildung 16.3 So werden Rechtecke im Berichtsdiagramm gezeichnet.
Aufmerksame Leser werden die Nachteile dieser Methode erkennen – einer davon ist, dass die Höhe auf dem größten Datenwert beruht, was bedeutet, dass große Werte ein überproportional großes Diagramm generieren. Wir haben das so gemacht, damit das Beispiel einfach bleibt, doch wenn wir wirklich mit dem Bericht arbeiten, soll bei den Berechnungen natürlich diese Höhe proportional zur Seitenhöhe sein. Weil wir die monatliche Performance unserer Site ausgeben wollen, ist ein Balkendiagramm die passende Wahl. Wenn wir es mit einer Datensorte zu tun haben, bei der ein Tortendiagramm geeigneter wäre, müssten wir Kreise oder Ellipsen zeichnen.
16.6.4 Das Zeichnen von Kreisen und Ellipsen Bevor Sie sich zu früh freuen: Wir werden in diesem Abschnitt nicht wirklich ein Tortendiagramm zeichnen, sondern Ihnen nur die Grundlagen des Zeichnens von Kreisen und Ellipsen zeigen. Die Befehle dafür sind einander ziemlich ähnlich, und der Unterschied liegt in der Art, wie sie gezeichnet werden. Ein Kreis wird um die Länge eines Radius gezeichnet, ausgehend von einem Zentrum, das von den x- und y-Werten festgelegt ist – siehe den folgenden Befehl: $page->drawCircle($x, $y, $radius, $startwinkel, $endwinkel, $fuelltyp);
379
16 PDFs erstellen Eine Ellipse hingegen wird in einer rechteckigen Begrenzungsbox gezeichnet, die mit zwei Sets von x- und y-Koordinaten wie drawRectangle() angegeben wird: $page->drawEllipse($x1, $y1, $x2, $y2, $startwinkel, $endwinkel, $fuelltyp);
Bei Tortendiagrammen sind die Argumente $startwinkel und $endwinkel zum Zeichnen von „Tortenstücken“ praktisch (siehe Abbildung 16.4). 90º
90º
360º
0º
Abbildung 16.4 Kreisabschnitte werden entgegen der Uhrzeigerrichtung gezeichnet.
Kreise werden entgegen der Uhrzeigerrichtung gezeichnet. Also beginnt das Beispiel in der Abbildung 16.4 links bei 90 Grad und geht weiter bis zu 0 oder 360 Grad. Das kann mit folgendem Code gezeichnet werden: $this->_page->drawCircle(300, 300, 50, deg2rad(90), deg2rad(360));
Das Beispiel im Bild rechts hat einen Startwinkel von 0 Grad und einen Endwinkel von 90 Grad, und das sieht im Code wie folgt aus: $this->_page->drawCircle(300, 300, 50, 0, deg2rad(90));
Das letzte Argument für beide Methoden wird verwendet, um den Fülltyp zu definieren, und zwar auf die gleiche Weise wie beim Zeichnen von Rechtecken. Ihnen ist wahrscheinlich aufgefallen, dass wir im obigen Code mit der PHP-Funktion deg2rad() arbeiten, um die Winkel anzugeben. Das liegt daran, dass Winkel als Radianten angegeben werden und nicht in Grad. Das mag Ihnen plausibel erscheinen oder auch nicht, zumindest ist es konsistent, weil das auch der Wert ist, den wir beim Drehen von Objekten verwenden.
16.7 Objekte drehen Sie rotieren ein Objekt, indem erst die Seite gedreht wird, dann das Element gezeichnet und schließlich die Seite auf ihre ursprüngliche Position (oder den nächsten angegebenen Winkel) zurückgedreht wird. Das mag einem anfangs komisch vorkommen, doch wenn man sich den Befehl anschaut, wird die Logik nachvollziehbar: $page->rotate($x, $y, $angle);
Indem die Seite über einen Winkel (in Radianten) um eine x- und y-Position gedreht wird, können wir jedes beliebige Objekt drehen, sei es Text, ein Rechteck, Kreis oder irgendein anderes Element, ohne dass jedes Element seine eigene Art der Rotation erfordert.
380
16.8 Bilder auf der Seite einfügen In Abbildung 16.5 demonstrieren wir die Seitendrehung, indem zuerst die Seite 10 Grad aus ihrer normalen Position gedreht und dann der Text oben auf der Seite gezeichnet wird. An diesem Punkt könnte man auch noch mehr Objekte zeichnen, und auch sie würden die Drehungseinstellungen übernehmen. Die Seite wird dann auf ihre Originalposition zurückgedreht, wobei der Text im Winkel von 10 Grad zur Seite bleibt. 10º
10º dreh m
ich
dreh m
ich
dreh m
ich
Abbildung 16.5 Demonstration der Drehung, indem Text in einem Winkel von 10 Grad gedreht wird
Damit Sie die Seite einfacher auf den ursprünglichen Winkel zurückdrehen können, speichern Sie den Grafikstatus, führen die Rotation(en) aus, zeichnen das Element und stellen dann den Grafikstatus wieder her: $page->saveGS(); $page->rotate($x, $y, $angle); // Zeichnen Sie hier Ihr Element $page->restoreGS();
Die Drehung wird nur auf jene Elemente angewendet, die Sie nach der Rotation zeichnen. Alles andere bleibt so, wie es vor der Rotation war. Nun haben wir schon eine Menge verschiedener Objekte auf der Seite besprochen, aber ein recht wichtiges ausgelassen: die Bilder.
16.8 Bilder auf der Seite einfügen Bilder werden im PDF-Dokument eingefügt, indem man zuerst die Bilddatei lädt, die in den Formaten TIFF, PNG oder JPEG vorliegen muss, den vollständigen Pfad an die Methode Zend_Pdf_Image::imageWithPath() übergibt und schließlich wie folgt auf der Seite einfügt: $file = '/Pfad/zum/Bild.jpg'; $image = Zend_Pdf_Image::imageWithPath($file); $page->drawImage($image, $x1, $y1, $x2, $y2);
Die x- und y-Werte hier sind die gleichen, wie sie zum Zeichnen eines Rechtecks genommen werden. Das Bild wird gestreckt, um es für die angegebene Größe passend zu machen, falls es nicht genauso groß (oder proportional dazu) ist wie die ursprüngliche Bildgröße. Wenn Sie alternative Bildformate laden müssen oder eine bessere Größenanpassung brauchen, können Sie auch mit der GD Library arbeiten, um eine modifizierte Version der Bilddatei zu speichern, die in Ihr PDF geladen werden soll. In Fällen, wo nur ein Teil des Bildes oder Objekts im Dokument erscheinen soll, können Sie mit Schnittmasken arbeiten.
381
16 PDFs erstellen
16.9 Objekte mit Schnittmasken zeichnen Alle die in diesem Kapitel beschriebenen Formen können auch als Schnittmasken verwendet werden, um Teile eines Bildes zu verdecken, z. B. einen unerwünschten Hintergrund in einem Bild. Die Methoden zum Erstellen der Schnittmasken sind fast identisch mit den Methoden zum Zeichnen der Formen, außer dass es keine Fülltypen gibt: $page->clipCircle(...); $page->clipEllipse(...); $page->clipPolygon(...); $page->clipRectangle(...);
Die Maske wirkt sich auf alle Elemente aus, die nach Definition einer Schnittmaske gezeichnet werden. Wenn Sie mit Elementen weitermachen wollen, die nicht von der Schnittmaske betroffen sind, speichern Sie den Grafikstatus, definieren eine Schnittmaske, zeichnen die Elemente und stellen den Grafikstatus wieder her. Wir haben nun die wichtigsten Funktionen von Zend_Pdf erläutert, es bleiben also nur noch die Models für den Berichtsgenerator übrig.
16.10Generierung von PDF-Berichten In diesem Kapitel sind wir die verschiedenen Teile des Berichtsdokuments und des SeitenModels durchgegangen, aber deren Verwendung haben wir noch nicht demonstriert. Die eigentliche Report_Page-Klasse ist ein ganzes Stück größer als das, was wir in diesem Kapitel zeigen konnten, aber die wesentlichen Methoden konnten wir besprechen. Im zu diesem Buch gehörigen Quellcode finden Sie die vollständige Klasse. Die letzte Methode, bevor wir zum Controller kommen, ist die Report_Page::render()Methode in Listing 16.8. Listing 16.8 Die render()-Methode der Report_Page-Klasse public function render() { $this->setStyle(); $this->setHeader(); $this->wrapText($this->_introText); $this->getGraphSection(); $this->getNotesSection(); $this->setFooter(); return $this->_page; }
Diese Methode ruft einfach die verschiedenen Bestandteile unserer Berichtsseite auf und gruppiert sie, damit wir sie zu einem späteren Zeitpunkt in ein Interface oder eine abstrakte Klasse verschieben können, falls die Ansprüche an die Berichte wachsen. Sie sollten alle hier aufgerufenen Methoden wiedererkennen – außer die setFooter()-Methode, die wie der Name schon sagt, eine Fußzeile auf der Seite produziert.
382
16.10 Generierung von PDF-Berichten Nach dieser letzten Methode wenden wir uns Listing 16.9 zu, in dem unsere Models das PDF-Dokument innerhalb der indexAction()-Methode der Controller-Klasse ReportController konstruieren. Listing 16.9 Die Berichts-Models in der Controller-Action ReportController public function indexAction() { $report = new Report_Document;
Erstellt
DokumentObjekt
Erstellt
Seiten-Objekt $page1 = new Report_Page; $page1->setYear(2008); Setzt ein $page1->setHeadTitle('Places reviews'); paar Textinhalte $page1->setIntroText('This is the report for reviews. Reviews are very important to Places to take the kids because they are an indication not only of how many people are reading the places information but of how confident users are in the community element of the site.'); Setzt einige Diagrammdaten
Nach Erstellen eines Dokument-Objekts n und eines Seiten-Objekts o fügen wir Text in das Seiten-Objekt ein p. Anschließend ergänzen wir die Diagrammdaten q, die in einer echten Situation reale Daten wären, die aus einer anderen Quelle wie z. B. einer Datenbank stammen. Hier sind es allerdings nur fiktive Daten. Da das Seiten-Objekt nun vollständig ist, können wir es ins Dokument einfügen r. An dieser Stelle könnten wir den Prozess wiederholen und so viele Seiten ins Dokument einfügen wie nötig. Das überspringen wir jetzt aber und generieren das Dokument: Wir starten mit Setzen des korrekten HTTP-Headers, der an den Browser geschickt werden soll s, und geben das Dokument aus, indem es mit der render()-Methode aus Listing 16.9 gerendert wird t. Schließlich werden View- und Layout-Rendering für die Action deaktiviert u, weil wir ja ein PDF-Dokument ausgeben. Das fertig gerenderte Dokument sehen Sie in Abbildung 16.6.
383
16 PDFs erstellen
Places reviews This is the report for reviews. Reviews are very important to Places to take the kids because they are an indication not only of how many people are reading the places information but of how confident users are in the community element of the site.
Abbildung 16.6 Die endgültige PDF-Berichtsseite, die der Berichtsgenerator erstellt hat
Nach Erstellung des PDF-Berichtsgenerators freuen wir uns nun auf viele spannende Diskussionen zur Weiterentwicklung der Site und sind in den Meetings mit akkuraten und aktuellen Informationen über unsere Website gewappnet!
16.11Zusammenfassung Dieses Kapitel hatte das Thema, wie man mit Zend_Pdf ein PDF-Dokument lädt bzw. erstellt, Metainformationen und Seiten einfügt und dann alles speichert. Sie haben erfahren, wie man für die Seite Styles wie Farben und Fonts setzt, die als Standard für alle gezeichneten Objekte angewendet werden. Außerdem haben Sie gelernt, wie man Formen und Text zeichnet, stylt und dreht, Bilder einfügt und Schnittmasken einsetzt. Unterwegs werden Ihnen sicher einige der Macken aufgefallen sein, auf die wir zu Beginn des Kapitels angespielt haben, was sich auf die Arbeit mit der aktuellen Version von Zend_Pdf bezog. Hoffentlich ist Ihnen ebenfalls aufgefallen, dass sie auch recht einfach zu umgehen sind (sicher werden diese Macken bei der Weiterentwicklung der Komponente noch ausgebügelt). Da wir grad von Weiterentwicklung sprechen: Seitdem wir mit dem Schreiben dieses Buches begonnen haben, hat die Entwicklung des Zend Frameworks ein Tempo bekommen, mit dem man manchmal schwer mithalten konnte. Da wir nun am Ende unserer Ausführungen über das Zend Framework angekommen sind, wäre unsere Empfehlung zum Abschied, dass Sie zum Community-Bereich der Zend Framework-Website gehen (http://framework.zend.com/community/overview) und sich in die Mailing-Listen eintragen, die Development-Newsfeeds abonnieren, mal im IRC-Channel #zftalk vorbeischauen und alles machen, was Sie bei diesen Entwicklungen auf dem Laufenden hält. Wir bedanken uns bei allen unseren Lesern und möchten Sie herzlich dazu einladen, uns auch persönlich – sei es negativ oder positiv – zu sagen, was Sie von diesem Buch halten, wenn wir uns mal bei einem Zend Framework-Treffen über den Weg laufen.
384
A
Die PHP-Syntax im Schnelldurchgang
Die Themen dieses Anhangs
Grundlagen der PHP-Syntax Variablen und Typen Wie Schleifen und Bedingungen in PHP funktionieren Die Grundlagen der PHP-Funktionen Zend Framework ist ein leistungsfähiges Framework und hat das Interesse einer großen Bandbreite von Usern geweckt. Darunter finden sich sowohl PHP-Entwickler, die ihre Sites und Fähigkeiten weiterentwickeln wollen, als auch Entwickler, die in anderen Sprachen und Umgebungen gearbeitet haben und nun Sites mit PHP erstellen wollen. Der Inhalt dieses Buches richtet sich nicht an Anfänger, und der Code setzt einen gewissen Grad an PHP-Kenntnissen voraus. Dieser Anhang und der nächste sind als schnelle Tour durch die zentralen PHP-Konzepte gedacht, über die Sie Bescheid wissen müssen. Natürlich ersetzen diese Anhänge kein vollständiges Buch zum Thema. Dieser Anhang stellt die zentralen Punkte der PHP-Sprache vor, die Sie kennen müssen, um mit diesem Buch arbeiten zu können. Es ist für Personen gedacht, die sich mit Programmierung auskennen, aber bei den Feinheiten von PHP nicht sonderlich versiert sind. Wenn Sie sich mit den Grundlagen der Programmierung nicht auskennen, schlagen wir vor, dass Sie sich PHP 5.3 und MySQL 5.1-Kompendium: Dynamische Webanwendungen Web Development von Welling und Thomson besorgen. Das ist eine ausgezeichnete Einführung in die Entwicklung mit PHP und die Programmierung fürs Web. Ein weiteres Buch, das über die Grundlagen hinausgeht, ist PHP in Action von Reiersøl, Baker und Shiflett. Wir beginnen unsere Tour mit einem Blick auf die Grundlagen von PHP und machen dann bei den häufigsten PHP-Operationen mit Arrays und String-Manipulationen weiter. Schließlich schauen wir uns an, wie PHP mit einem Webserver zusammenarbeitet. Anhang B setzt
385
A Die PHP-Syntax im Schnelldurchgang darauf auf und erläutert die Objektorientierung in PHP, außerdem die anspruchsvolleren Konzepte der Standard PHP Library und der Software-Designpattern.
A.1 PHP-Grundlagen PHP ist eine sehr pragmatische Sprache, die so entworfen wurde, dass Sie damit schnell und einfach Webseiten erstellen können. Vieles von der Syntax hat PHP von C und Perl geerbt und sich für seine objektorientierten Ergänzungen noch ein paar inspirative Brocken aus Java geholt. Listing A.1 zeigt ein einfaches Programm mit grundlegenden Konstrukten. Listing A.1 Das einfache PHP-Skript listing1.php
Deklariert Funktion
say('Rob'); say('Nick'); ?>
Um diesen Code zu starten, geben wir am Prompt einfach php listing1.php ein. Alternativ können wir auch in einem Browser zur Datei navigieren, wenn sie sich im RootVerzeichnis eines Webservers befindet. Dieser Code ist sehr einfach und produziert folgende Ausgabe: Hallo Rob Guten Tag Nick
Wie Sie in Listing A.1 sehen, endet jede Zeile bei PHP mit einem Semikolon, und alle Strukturen werden in geschweiften Klammern eingefasst. Weil PHP als Web-Sprache außerdem so designt wurde, dass sie mit HTML vermischt werden kann, müssen Sie einen PHP-Code-Block mit der Anweisung beenden. Wenn in der Datei kein weiterer Code enthalten ist, können Sie das schließende ?> weglassen. Eine Funktion in PHP startet mit dem Schlüsselwort function, gefolgt vom Namen der Funktion und runden Klammern, die die Argumente für die Funktion einfassen n. Der Inhalt der Funktion wird in geschweifte Klammern gesetzt und wie gezeigt ausgeführt q. Alle bedingten Anweisungen haben die gleiche Grundstruktur wie die if()-Anweisung o. Bei PHP gibt es zwei wesentliche Arten von Strings: Strings in einfachen und in doppelten Anführungszeichen. Bei der Verwendung von einfachen Anführungszeichen wird
386
A.2 Variablen und Typen der Text exakt so ausgegeben, wie er eingetippt wurde, doch bei doppelten Anführungszeichen werden alle Variablen innerhalb des Strings auf den Wert der Variable geändert p (auch als Variableninterpolation bezeichnet). PHP-Programme werden gewöhnlich auf viele verschiedene Dateien aufgeteilt. Anhand der Schlüsselwörter include und require wird eine Datei in einer anderen Datei geladen und ausgeführt. Um beispielsweise die Datei b.php zu laden, nimmt man den folgenden Code: include 'b.php';
Wenn die Zahl der Dateien in einem Projekt wächst, fällt der Überblick ziemlich schwer, wo eine bestimmte Datei eingebunden wird. Um zu verhindern, dass die gleiche Datei doppelt geladen wird, kann man mit den Schlüsselwörtern include_once und require_once arbeiten. Diese arbeiten genauso wie include und require, außer dass die Datei nicht ein zweites Mal geladen wird, falls sie schon geladen wurde. Schauen wir uns genauer an, wie diese grundlegenden Konstrukte in PHP funktionieren, und beginnen mit Variablen und Typen.
A.2 Variablen und Typen Variablen fangen in PHP mit einem $-Symbol an und sind schwach typisiert – ganz im Gegensatz zu C oder Java, wo die Variablen stark typisiert sind. Das bedeutet, dass Sie jeden beliebigen Datentyp ohne Probleme in einer Variablen speichern können. Außerdem konvertiert die Sprache automatisch zwischen Typen, falls der Verwendungskontext das erfordert. Wenn Sie beispielsweise einen String in der einen Variable haben und eine Zahl in der anderen, wird PHP den String in eine Zahl konvertieren, um beides addieren zu können. Eine vollständige Erklärung der Regeln, mit denen PHP die Typen verändert (das sogenannte Type Juggling), finden Sie unter http://www.php.net/language.types.type-juggling. Tabelle A.1 listet die wichtigsten Typen aus PHP auf. Tabelle A.1 Datentypen in PHP Datentyp
Beschreibung
boolean
Ein skalarer Typ, der entweder true oder false ist (case insensitive, Groß/Kleinschreibung wird also nicht beachtet).
int
Jede Zahl ohne Dezimalpunkt wie z. B. –2 oder 0x34 (hexadezimal).
float
Jede beliebige Präzisionszahl mit Dezimalpunkt wie z. B. 3.142 oder 6.626e–34.
string
Jede Reihe von Zeichen, die alle ein Byte lang sind, wie z. B. “hello world”.
array
Eine Zuordnung (Map) von Werten und Schlüsseln. Der Schlüssel kann entweder numerisch sein und bei 0 anfangen oder ein beliebiger String wie $a=array('a', 'b', 'c'); oder $a[0]='a';.
387
A Die PHP-Syntax im Schnelldurchgang Datentyp
Beschreibung
object
Eine Gruppierung von Variablen und Funktionen wie class user { protected $_name; function getName() { return $this->_name; } }
resource
Ein Verweis auf eine externe Ressource wie ein Dateizeiger oder ein DatenbankHandle. Eine Ressource wird beispielsweise erstellt, wenn man wie folgt eine Datei öffnet: $fp=fopen('Dateiname');
null
Eine Variable ohne Wert. Das Schlüsselwort null ist case insensitive.
Bei PHP müssen Sie eine Variable vor der ersten Verwendung nicht deklarieren. Die Variable wird erstellt, sobald ihr ein Wert anhand von = zugewiesen wird. Falls sie bereits existiert, wird der Wert dieser Variable überschrieben. Es ist bei PHP auch möglich, eine „Variablenvariable“ zu erstellen. Das ist eine Variable, deren Name dynamisch anhand einer weiteren Variable gesetzt wird. Hier folgt ein Beispiel: $a = 'name'; $$a = 'Rob';
Das bedeutet, dass es hier nun eine Variable namens $name gibt, deren Wert Rob ist. Von daher können wir den Wert wie folgt ausgeben: echo $name;
Das wird die Ausgabe Rob produzieren. Der String-Typ in PHP ist umfassend ausgestattet und weist einige Finessen auf, die wir uns als Nächstes anschauen wollen.
A.3 Strings Strings können in PHP auf vier verschiedene Weisen deklariert werden:
In einfachen Anführungszeichen In doppelten Anführungszeichen Heredoc-Syntax Nowdoc-Syntax (PHP5.3 oder höher) Das schauen wir uns nun der Reihe nach an.
388
A.3 Strings A.3.1.1
Strings in einfachen Anführungszeichen
Der einfachste String ist der String-Literal, der wie folgt über das einfache Anführungszeichen definiert wird: $name = 'Rob';
Es gibt zwei spezielle Zeichen in einem String-Literal: das einfache Anführungszeichen und der Backslash. Um diese Zeichen in einem String zu nutzen, müssen Sie diese wie folgt mit einem Backslash escapen: $name = 'Geht\'s dir gut?';
Wenn Sie einen Backslash im String-Literal speichern wollen, können Sie das mit einem Backslash escapen, was als doppelter Backslash bezeichnet wird. A.3.1.2
Strings in doppelten Anführungszeichen
Mit Strings in doppelten Anführungszeichen kann man spezielle Escape-Sequenzen schreiben, die für besondere Zeichen benutzt werden. Dazu gehören \n für den Zeilenvorschub und \t für Tabulator. Sie können beispielsweise einen mehrzeiligen String wie folgt schreiben: echo "Das ist Zeile eins\nDas ist Zeile zwei\n";
Sie können einen String auch über mehrere Zeilen in der Datei erstellen, und PHP wird ihn ebenfalls mehrzeilig ausgeben. Die vollständige Liste von Escape-Zeichen finden Sie online im PHP-Manual unter http://www.php.net/manual/en/language.types.string.php. Auch hier nehmen Sie einen doppelten Backslash, wenn Sie im String einen Literal-Backslash brauchen. Wichtiger noch ist, dass (wie beim Listing A.1 angemerkt) Variablen innerhalb von doppelten Anführungszeichen durch ihre Werte ersetzt werden. Es gibt zwei Arten der Syntax für das Parsing von Variablen innerhalb von Strings: einfache und komplexe (bei der runde Klammern in die einfache Syntax eingefügt werden). Öfter wird ein einfaches Variablen-Parsing eingesetzt. Wenn sich im String ein DollarZeichen befindet, nimmt PHP die nächste Zeichensequenz bis zum Satzzeichen als Variablenname, der erweitert werden soll. Listing A.2 zeigt Beispiele einfacher Variablenexpansion innerhalb von doppelten Anführungszeichen. Listing A.2 Einfache Variablenexpansion in Strings name.";
Gibt den Wert des ArrayElements aus
Gibt den Wert der Eigenschaft des Objekts aus
?>
389
A Die PHP-Syntax im Schnelldurchgang Beachten Sie, dass die Verwendung eines Arrays in einem String ein Sonderfall ist. Normalerweise müssen Sie einen String mit Anführungszeichen in den eckigen Klammern eines Arrays eingrenzen. Bei der komplexen Syntax wird der Variablenname einfach in geschweifte Klammern gesetzt (siehe Listing A.3). Listing A.3 Komplexe Variablenexpansion in Strings
Gibt den Wert eines mehrdimensionalen Arrays aus
echo "My name is {$users['names']['rob']}."; echo "My name is {$user->getName()}.";
Gibt das Ergebnis einer Elementmethode eines Objekts aus
?>
Wie Sie sehen, können Sie mehrdimensionale Arrays verwenden, wenn Sie mit der Syntax der komplexen Variablenexpansion arbeiten, und es funktionieren sogar Objekt-Methoden. A.3.1.3
Heredoc-Strings
Bei Heredoc-Strings können innerhalb eines String-Blocks sowohl einfache als auch doppelte Anführungszeichen verwendet werden, ohne dass sie escapet werden müssen. Dieses Format verwendet den Operator <<<, gefolgt von einem Identifikator, um den String zu starten. Der String wird terminiert, wenn man das nächste Mal auf den Identifikator am Beginn einer eigenen Zeile trifft. Hier folgt ein Beispiel: $name = 'Rob'; echo <<<EOT Da ist "$name". EOT;
Das führt zu diesem Output: Da ist "Rob".
Wie Sie sehen können, werden alle Variablen in einem Heredoc-String unter Beachtung der gleichen Regeln wie für Strings in doppelten Anführungszeichen erweitert. A.3.1.4
Nowdoc-Strings
Nowdoc-Strings sind einfach Heredoc-Strings ohne irgendwelches Parsing. Sie funktionieren genau wie Strings in einfachen Anführungszeichen, verwenden aber die HeredocSyntax. Sie werden genauso wie Heredoc-Strings erstellt, außer dass der Identifikator in einfache Anführungszeichen gesetzt wird. Hier folgt ein Beispiel: $name = 'Rob'; echo <<<'EOT' Da ist "$name". EOT;
390
A.4 Arrays Dieser Code führt zu diesem Output: Da ist "$name".
Das heißt, Nowdoc-Strings sind ideal für große Textmengen wie z. B. PHP-Code, ohne dass man sich Gedanken über das Escaping machen muss. Das sind alle Möglichkeiten, wie man Strings erstellen kann. Machen wir also mit der Array-Syntax weiter.
A.4 Arrays Arrays werden für viele unterschiedliche Datenstrukturen in PHP eingesetzt, weil es viele Funktionen gibt, die Arrays manipulieren können. Arrays können als Arrays, Listen, HashTabellen, Dictionarys, Collections, Stacks und Queues behandelt werden. Im Vergleich zu Strings sind Arrays relativ simpel, weil man sie nur auf zweierlei Weise spezifizieren kann. Bei der einen setzt man das Schlüsselwort array() wie folgt ein: $users = array('Rob', 'Nick', 'Steven');
Bei der anderen Weise wird direkt zugewiesen: $users[] = 'Rob'; $users[] = 'Nick'; $users[] = 'Steven';
Beide Beispiele führen zu einem Array mit drei Elementen mit den Indizes 0, 1 und 2. Arrays können auch assoziativ sein, wenn also wie folgt Strings als Schlüssel genommen werden: $book['author'] = 'Rob'; $book['reader'] = 'Fred';
Schließlich können Arrays auch mehrdimensional sein: $users = array( array('name'=>'Rob', 'country'=>'UK'), array('name'=>'Nick', 'country'=>'Australien'), array('name'=>'Steven', 'country'=>'Australien') ); print_r($users);
Das obige Beispiel generiert diesen Output: Array ( [0] => Array ( [name] => [country] ) [1] => Array ( [name] => [country] )
Rob => UK
Nick => Australien
391
A Die PHP-Syntax im Schnelldurchgang [2] => Array ( [name] => Steven [country] => Australien ) )
Beachten Sie, dass der Schlüssel für ein Array entweder ein Integer oder ein String sein kann. Wenn der Schlüssel nicht angegeben ist, wird der maximale Integer, der bereits im Array verwendet wird, um 1 erhöht. Wenn es im Array keinen Integer gibt, wird 0 verwendet. Wir haben uns nun all die Datentypen in PHP angeschaut und machen jetzt mit Schleifenund Bedingungsstrukturen weiter.
A.5 Bedingungen und Schleifen So wie ein PHP-Skript einfach eine Serie von Anweisungen ist, sind die Schleifen- und Kontrollstrukturen der Teil des Skripts, der die eigentliche Arbeit ausführt, um ein bestimmtes Ziel zu erreichen. Mit einer Schleife kann der gleiche Code mehrmals ausgeführt werden, meistens mit geänderten Variablen, sodass ähnliche, aber unterschiedliche Resultate erscheinen. Durch Bedingungen verzweigt der Code, sodass nur bestimmte Teile des Codes ausgeführt werden, wenn bestimmte Bedingungen erfüllt sind – und damit machen wir weiter.
A.5.1 Bedingungen Es gibt zwei Hauptkonstrukte für Bedingungen: if und switch. Die Syntax der ifAnweisung wird in Listing A.4 gezeigt. Listing A.4 Die if()-Syntax echo } elseif echo } else { echo } ?>
b) { '$a is bigger than $b'; ($b > $a) { '$b is bigger than $a';
Prüfen auf alternative Bedingung
'$a and $b are equal';
Listing A.4 zeigt, dass ein if-Konstrukt eine bedingte Anweisung (conditional statement) in runden Klammern enthält, und der auszuführende Code, wenn die Bedingung true ergibt, steht zwischen den geschweiften Klammern. Die Anweisung elseif, die genauso funktioniert wie else if, sorgt dafür, dass mehrere Bedingungen getestet werden. Schließlich erlaubt eine else-Anweisung, dass ein Code-Block ausgeführt wird, falls alle anderen Bedingungen nicht erfüllt wurden.
392
A.5 Bedingungen und Schleifen Die switch-Anweisung ist im Wesentlichen eine Serie von if-else-Anweisungen, bei denen die gleiche Variable mit einer Serie von Alternativwerten verglichen wird (siehe Listing A.5). Listing A.5 Die switch()-Syntax
Beendet Ausführung in diesem Fall Wird ausgeführt, wenn kein anderer Fall passt
?>
Beachten Sie, dass die switch-Anweisung ab der ersten passenden Fallanweisung bis zum Ende weiter ausgeführt wird, wenn Sie kein break setzen. Das Schlüsselwort default passt, wenn alle vorangegangenen Fälle nicht zutrafen, und es muss als Letztes kommen, wenn Sie damit arbeiten wollen. Sowohl if als auch switch ändern basierend auf bestimmten Bedingungen den Fluss der Operation. Nun beschäftigen wir uns mit Schleifen, durch die man die gleiche Aufgabe mehrmals ausführen kann.
A.5.2 Schleifen Es gibt vier Hauptkonstrukte für Schleifen: while, do-while, for und foreach. Sie alle erlauben, dass eine Anweisung oder ein Block mehrmals ausgeführt werden. Wir beginnen mit der while-Schleife. A.5.2.1
Die Arbeit mit while() und do-while()
Die while-Schleife ist die einfachste. Hier folgt ein Beispiel: $i = 0; while ($i < 10) { $i++; echo "$i\n"; }
Durch diesen Code weiß PHP, dass der Block so oft wiederholt werden soll, bis $i 10 ist oder größer. Beachten Sie, dass die Ausführung erst am Ende des Blocks stoppt. Außerdem wird der Block gar nicht erst ausgeführt, falls die Bedingung sofort false ergibt. Wenn der Block zumindest einmal ausgeführt werden soll, nehmen Sie do-while wie folgt:
393
A Die PHP-Syntax im Schnelldurchgang $i = 0; do { echo "$i\n"; $i++; } while ($i < 10);
Im Fall von do-while wird die Bedingung am Ende jeder Iteration geprüft anstatt am Anfang. Sie können die Ausführung einer Schleife frühzeitig beenden, indem Sie die Anweisung break verwenden; mit der continue-Anweisung wird die Schleife erneut gestartet, bevor sie zum Ende dieser Iteration kommt (siehe Listing A.6). Listing A.6 Mit break und continue in Schleifen arbeiten
Gibt Ausführung an den
Start der Schleife zurück
Beendet die Schleife
} ?>
Der Code in Listing A.6 führt die Schleife 5 Mal aus und produziert die folgende Ausgabe: first 2 3 4
Die continue-Anweisung n schickt die Ausführung an die while()-Anweisung zurück, ohne dass $i ausgegeben wird. Die break-Anweisung o stoppt die while-Schleife komplett, wenn $i 5 ergibt. Die Schleifen while und do-while werden generell dann eingesetzt, wenn man die genaue Anzahl der Iterationen nicht kennt. Falls Sie wissen, wie oft die Schleife ausgeführt werden soll, nehmen Sie die for-Schleife. A.5.2.2
Die for()-Schleife
Die for-Schleifenkonstrukte sind die komplexesten in PHP, und sie funktionieren wie forSchleifen in C. Listing A.7 zeigt eine solche Schleife im Einsatz.
394
A.5 Bedingungen und Schleifen Listing A.7 Die for()-Syntax
Schleife wird 10 Mal durchlaufen
?>
Bei einer for-Schleife stehen in den Klammern drei Ausdrücke. Der erste Ausdruck ($i =0 in Listing A.7) wird einmal ausgeführt, bevor die Schleife beginnt. Der zweite Ausdruck ($i < 10 in Listing A.7) wird zu Beginn jeder Schleife ausgewertet, und wenn er true ergibt, wird der Anweisungsblock innerhalb der Klammern ausgeführt. Ergibt er false, endet die Schleife. Nachdem der Anweisungsblock ausgeführt wurde, wird der dritte Ausdruck ($i++ in Listing A.7) ausgeführt. Alle Ausdrücke können mehrere Anweisungen enthalten, die durch Kommas getrennt sind. Das letzte Schleifenkonstrukt foreach ist eine vereinfachte Version der for-Schleife, die durch Arrays iteriert. A.5.2.3
Die foreach()-Schleife
Die foreach-Schleife funktioniert nur bei Arrays und Objekten. Sie iteriert durch jedes Element im Array und führt der Reihe nach für jedes Element einen Anweisungsblock aus. Dies sehen Sie in Listing A.8. Listing A.8 Die foreach()-Syntax $user) { echo "$key: $user\n"; }
Weist bei jedem Durchlauf $value dem aktuellen Element zu
?>
Bei jedem foreach-Schleifendurchlauf wird der Wert des aktuellen Element $value zugewiesen und der Schlüssel an $key. Der Code in Listing A.8 ergibt den folgenden Output: 0: Rob 1: Nick 2: Steven
Der Schlüssel ist optional, aber der Teil mit dem Wert nicht. Beachten Sie, dass die WertVariable (in diesem Fall $user) eine Kopie des Werts aus dem $users-Array ist. Wenn Sie das ursprüngliche Array modifizieren wollen, müssen Sie darauf mit einem &-Symbol verweisen – also etwa wie folgt: foreach ($users as &$user) { echo "$key: $user\n"; }
395
A Die PHP-Syntax im Schnelldurchgang Das waren die wichtigen Schleifenkonstrukte in PHP. Um diesen Abschnitt abzuschließen, schauen wir uns noch eine alternative Syntax an, mit der wir auf die Klammern um die verschachtelten Code-Blöcke verzichten können.
A.6 Alternative Syntax für den verschachtelten Block Es gibt eine alternative Syntax für Steuerstrukturen mit Schleifen und Bedingungen, die in View-Skripten des Zend Frameworks allgemein verwendet werden. Diese Form ersetzt die öffnende Klammer durch einen Doppelpunkt und die schließende durch ein neues Schlüsselwort: endif, endwhile, endfor, endforeach oder endswitch (was passend ist). Dies sehen Sie in Listing A.9. Listing A.9 Alternative Syntax für Steuerstrukturen
Verwendet Doppelpunkt
anstatt einer öffnenden { b) : ?> $a is bigger than $b $a) : ?> $b is bigger than $a Beendet Anweisung mit $a and $b are equal abschließendem Schlüsselwort
Die alternative Version wird am häufigsten dann eingesetzt, wenn PHP direkt mit HTML vermischt wird. In diesem Szenario wird jede PHP-Anweisung von den öffnenden und schließenden PHP-Anweisungen eingefasst, sodass man zwischen jeden Bedingungstest einfaches HTML schreiben kann. Das Beispiel in Listing A.9 hat nur eine Textzeile, doch bei einer echten Applikation gäbe es normalerweise viele Zeilen. Die alternative Syntax wird genutzt, weil es relativ schwer ist, bei den Klammern den Überblick zu behalten, wenn man die PHP-Blöcke so oft öffnet und schließt. Nun kennen Sie die zentralen Features der PHP-Steuerstrukturen mit Schleifen und Bedingungen, und können damit fortfahren, wie der ausführbare Code über Funktionen organisiert wird.
A.7 Funktionen Bei einer Funktion handelt es sich um einen Code-Block, der vom Rest getrennt ist. Somit kann man einen Code-Block mehrfach verwenden, ohne den Code mit Copy & Paste mehrere Male einzusetzen. Der Code in einer Funktion wird außerdem vom restlichen PHP-Code isoliert und kann so einfacher getestet werden. Überdies sind wir so sicher, dass er nicht von anderem Code abhängig ist. Listing A.10 zeigt ein Beispiel für eine Funktion.
396
A.8 Zusammenfassung Listing A.10 Beispielfunktion zum Darstellen eines Namens
Deklariert Funktion
function hello($name) { echo 'Greetings '; echo ucfirst($name); return true;
Gibt Ergebnis eines
Funktionsaufrufs aus
Gibt Wert an
aufrufenden Code zurück
} ?>
Eine Funktion wird anhand des Schlüsselworts function deklariert, dem der Name der Funktion und runde Klammern folgen. Wenn die Funktion Parameter annimmt, werden diese zwischen den Klammern aufgeführt. Die Funktion in Listing A.10 nimmt einen Parameter ($name) und stellt ihn dar, nachdem sie über eine andere Funktion namens ucfirst() das erste Zeichen in einen Großbuchstaben konvertiert hat n. Anmerkung
Alle Funktionen befinden sich im globalen Geltungsbereich, und zwei Funktionen dürfen nicht den gleichen Namen tragen. Es gibt viele Tausende PHP-Funktionen innerhalb all der Extensions, die in PHP verfügbar sind. Also sollten Sie sich überlegen, Ihren Funktionen ein Präfix wie z. B. Ihre Initialen mitzugeben, um Konflikte zu vermeiden.
Die Funktion in Listing A.10 wird wie folgt ausgeführt: $result = hello('rob');
Sie führt zu diesem Output: Greetings Rob
Natürlich sind die meisten Funktionen deutlich komplexer als diese hier. Alle Variablen, die außerhalb des Geltungsbereichs einer Funktion erstellt werden, stehen außerhalb der Funktionsdefinition nicht zur Verfügung. Am einfachsten bekommt man eine Funktion dazu, Informationen herauszugeben, indem man das Schlüsselwort return verwendet o. Im Fall der Funktion hello() in Listing A.10 ist der Rückgabewert true, der dann der Variable $result zugewiesen wird.
A.8 Zusammenfassung Damit ist unsere Einführung in die Grundlagen der PHP-Syntax abgeschlossen. Es gibt natürlich noch viel mehr über diese Sprache zu sagen, und die feineren Details der Syntax und Verwendung sollten Sie in der Online-Dokumentation nachschlagen. Für PHP gibt es sehr viele Erweiterungen, durch die viele weitere Funktionen in dieser Sprache integriert werden. Durch diese große Bandbreite der Funktionen wird PHP als Websprache so leis-
397
A Die PHP-Syntax im Schnelldurchgang tungsfähig. Bei den meisten Erweiterungen geht es um Datenbanken, XML-Parsing, Dateisystemschnittstellen, Verschlüsselung, Zeichenkodierung, Bildbearbeitung, E-Mails etc. Weil dies ja nur eine Spritztour sein soll, bleibt neben dem Gesagten viel mehr Ungesagtes! Wir haben die zentralen Themen Variablen, Schleifen, Bedingungen und Funktionen auf einem hohen Niveau erläutert, was (zusammen mit Anhang B) ausreichen sollte, um den Code in diesem Buch nachvollziehen zu können. In der PHP-Sprachreferenz finden Sie weitere Einzelheiten über die Nuancen aller angesprochenen Themen (http://www.php .net/manual/de/langref.php). Im restlichen PHP-Manual werden die Tausenden Funktionen der Erweiterungen abgedeckt. Jede Funktion hat im Manual ihre eigene Seite, auf der Details über den Einsatz, Rückgabetypen und Beispiele vorgestellt werden. PHP5 hat die objektorientierten Features der Sprache außerordentlich verbessert. Weil Zend Framework ein objektorientiertes Framework ist, wird die gesamte Funktionalität in Form von Objekten bereitgestellt. In Anhang B beschäftigen wir uns mit den zentralen Features des objektorientierten PHP-Systems, die Sie kennen müssen, um das Zend Framework zu verstehen.
398
B
Objektorientiertes PHP
Die Themen dieses Anhangs
Das PHP-Objekt-Model Einführung in die Iteratoren, Arrays und Countable-Interfaces der Standard-PHPLibrary
Grundlagen von Software-Designpattern (z. B. Singleton und Registry) In diesem Anhang frischen Sie Ihr Wissen jener Bereiche von PHP auf, mit denen Sie bisher noch nicht zu tun hatten. Zend Framework ist in PHP5 geschrieben und komplett objektorientiert. Dafür brauchen Sie ein gewisses Grundverständnis von der objektorientierten Programmierung (OOP) in PHP, um sich darin zurechtzufinden. Dieses Kapitel kann natürlich kein vollständiges Buch über dieses Thema wie z. B. PHP in Action von Reiersøl, Baker und Shiflett ersetzen, doch es soll Ihnen so weit helfen, um mit Zend Framework an den Start zu kommen. Wir beschäftigen uns damit, was eine Klasse und die Deklaratoren zur Sichtbarkeit pubund private ausmacht. Und weil die Objektorientierung ihre volle Leistungsfähigkeit dann erreicht, wenn die Klassen erweitert werden, untersuchen wir auch den Einsatz abstrakter Klassen und Schnittstellen, um die API für eine Komponente besser zu dokumentieren.
lic, protected
Mit PHP5 wurde SPL eingeführt. Mit dieser Library können Klassen einfacher verwendet werden. Das Zend Framework arbeitet intern mit SPL. Also schauen wir uns an, welche Leistungen damit möglich werden. Ein weiteres Thema sind Software-Designpattern. Ein Designpattern ist bei einem bestimmten Problem die Vorgehensweise, die von vielen verwendet wird. Ich verwende den Begriff „Vorgehensweise“ absichtlich, weil der eigentliche Code, der zur Lösung des Problems verwendet wird, gewöhnlich unterschiedlich ist, aber allen Lösungen das gleiche Prinzip gemeinsam ist. Durch Designpattern bekommen wir ein Vokabular, mit dem wir Lösungen weitergeben und einander besser verstehen können. Ein weiterer Vorteil ist, dass
399
B Objektorientiertes PHP sie in echten Applikationen getestet wurden. Also können Sie sicher sein, dass das Designpattern eine gute Lösung für die Klasse der Probleme ist, für die es gedacht ist. Also frisch ans Werk und gleich ran an die Objektorientierung.
B.1 Objektorientierung in PHP Die objektorientierte Programmierung (OOP) ist eine Möglichkeit, um zusammengehörige Funktionen und Variablen zu gruppieren. Das fundamentale Ziel ist, einen Code zu produzieren, der sich leichter pflegen und warten lässt – also einen Code, der nicht versehentlich kaputt geht, wenn Sie an etwas anderem arbeiten. In diesem Abschnitt schauen wir uns die Grundlagen von Klassen und Objekten an und wie sie über Vererbung miteinander in Bezug gesetzt werden. Dann geht es darum, wie man mit Interfaces und abstrakten Klassen die Verwendung von Klassen steuert. Schließlich beschäftigen wir uns mit den sogenannten „magischen“ Methoden von PHP5.
B.1.1 Klassen, Objekte und Vererbung Um die OOP zu begreifen, müssen Sie Klassen und Objekte verstehen. Da wir grad dabei sind, schauen wir uns auch Interfaces an, die eng mit Klassen zusammenhängen. Eine Klasse ist der Code, der Methoden und Variablen miteinander gruppiert. Das ist schon alles. Also keine Bange! Listing B.1 zeigt die einfachste Klasse. Listing B.1 Die einfachste Implementierung einer Klasse class ZFiA_Person {
Definiert den
Namen der Klasse
}
Wie Sie sehen, wird über das Schlüsselwort class die Klasse deklariert. Dem folgt der Name der Klasse, in diesem Fall ZFiA_Person E. Der Klassenname darf in all Ihren Dateien nur einmal vorkommen. Also ist es eine gute Idee, jedem Klassennamen einen eindeutigen Identifikator voranzustellen. In unserem Fall nehmen wir das Präfix „ZFiA“. Das ist zwar anfänglich beim Schreiben des Codes etwas mehr Arbeit, doch später profitieren Sie davon, wenn Sie Ihren Code mit dem von jemand anderem integrieren müssen, und damit lohnt sich jede Mühe. Außerdem wird die Verwendung eines Präfixes vom PHP.NetManual empfohlen. Ein Objekt ist eine Laufzeitinstanziierung einer Klasse. Das bedeutet, dass es einen Namen trägt und anhand des Schlüsselworts new erstellt wird: $rob = new ZFiA_Person();
400
B.1 Objektorientierung in PHP Die Variable $rob ist ein Objekt, und daraus folgt, dass man viele Objekte des gleichen Typs haben kann. Man kann beispielsweise mit der gleichen Syntax das Objekt $nick erstellen: $nick = new ZFiA_Person();
Nun haben wir zwei Objekte, die von der gleichen Klasse sind. Das heißt, es gibt nur eine Klasse, aber zwei Instanzen von Objekten darin. Sie können beliebig viele Objekte aus einer bestimmten Klasse erstellen. Schauen wir uns an, wie man das Objekt anhand eines Konstruktors einrichtet, damit es sofort nach der Erstellung einsatzfähig ist. B.1.1.1
Konstruieren und löschen
Normalerweise muss ein Objekt seinen internen Status einrichten, d.h. es muss die anfänglichen Werte seiner Elementvariablen setzen. Dafür braucht man eine spezielle Methode namens Konstruktor, die den Namen __construct() trägt. Diese Methode wird automatisch aufgerufen, sobald ein neues Objekt instanziiert wird. Die meisten Klassen verfügen über einen Konstruktor, und gewöhnlich werden ein oder mehrere Funktionsparameter verwendet, um den internen Status des Objekts zu setzen. Dies sehen Sie in Listing B.2. Am anderen Ende de Objektlebenszeit wird eine weitere Methode namens __destruct() aufgerufen, kurz bevor das Objekt gelöscht werden soll. Diese Methode heißt Destruktor und kommt bei fürs Web gedachten PHP-Skripten kaum zum Einsatz, weil es in der Natur von PHP liegt, jede Anfrage nach dem Erstellen auch wieder zu löschen. Bei PHP werden alle Objekte am Ende der Anfrage automatisch gelöscht und zu Beginn der nächsten Anfrage neu erstellt. Listing B.2 Zur Klasse aus Listing B.1 einen Konstruktor hinzufügen
Listing B.2
Adding a constructor to our class from listing B.1
Deklariert Konstruktormethode
Deklariert Elementvariablen,
die nur für diese Klasse sichtbar sind
public function __construct($firstName, $lastName) { $this->_firstName = $firstName; Weist Elementvariablen $this->_lastName = $lastName; Funktionsparameter zu }
}
Listing B.2 stellt die Schlüsselwörter private und public vor, die die Sichtbarkeit der Methoden und Variablen in einer Klasse steuern. Diese werden im nächsten Abschnitt eingehender erläutert. In Listing B.2 sehen Sie, dass wir zwei Elementvariablen (member variables) erstellt haben n, deren Anfangswerte anhand des Konstruktors gesetzt werden o.
401
B Objektorientiertes PHP B.1.1.2
Die Sichtbarkeit über public, protected und private steuern
Wie bereits angemerkt kann eine Klasse Methoden und Variablen aufweisen. Diese Gruppierung ist an sich schon hilfreich, um den Code zu organisieren, aber das wird erst dann richtig von Bedeutung, wenn wir die Datenkapselung (Information Hiding) hinzunehmen. Anmerkung
Datenkapselung ist die Fähigkeit, Methoden und Variablen von Klassen als außerhalb der Klassengrenzen unsichtbar zu markieren. Das führt dazu, dass Code besser zu pflegen ist, weil jede Klasse eine API „veröffentlicht“, die andere Klassen verwenden sollen, aber diese API nach Belieben implementiert werden kann.
Daran sind drei Schlüsselwörter beteiligt: public, protected und private. Sie werden genauso wie in anderen objektorientierten Sprachen wie C++, Java oder C# eingesetzt.
public: Im gesamten Geltungsbereich verfügbar. protected: Nur innerhalb der Klasse oder deren untergeordneten Klassen verfügbar. private: Nur verfügbar innerhalb der definierenden Klasse selbst. Schauen wir uns an, wie das eingesetzt wird, und arbeiten die Klasse ZFiA_Person in Listing B.3 noch etwas aus. Listing B.3 Eine einfache Klasse
Deklariert die Elementvariablen, die für die Klasse und deren Kinder sichtbar sein sollen
public function __construct($firstName, $lastName) { $this->_firstName = $firstName; Weist den $this->_lastName = $lastName; Elementvariablen zu } public function fullName() { return $this->_firstName . ' ' . $this->_lastName; }
Definiert Methoden, die von den Weiterverarbeitern der Klasse verwendet werden sollen
}
Wie Sie sehen, haben wir die Elementvariablen $_firstName und $_lastName als protected gekennzeichnet. Weil von Benutzern dieser Klasse nicht darauf zugegriffen werden kann, geben wir eine Methode namens fullName() an, um den Zugriff auf die Daten zu erlauben. Wir dürfen nun frei wählen, wie die Informationen über Vor- und Nachname gespeichert werden, ohne dass sich das auf irgendwelchen Code auswirkt, der diese Klasse verwendet.
402
B.1 Objektorientierung in PHP Wir sollten auch vermeiden, diesen Code für eine Klasse zu wiederholen, die vom Konzept her ähnlich ist. Darum kümmert sich das Konzept der Klassenerweiterung.
B.1.2 Erweitern von Klassen Die Erweiterung von Klassen eröffnet erstaunliche Möglichkeiten und spart viel Zeit, weil damit die Wiederverwendbarkeit von Code erleichtert wird. Wenn eine Klasse eine andere erweitert, erbt sie deren Eigenschaften und Methoden, vorausgesetzt, dass diese nicht private sind. Die zu erweiternde Klasse nennt man die übergeordnete oder Elternklasse, und die sie erweiternde Klasse ist die untergeordnete oder Kindklasse. Wenn Sie eine andere Klasse erweitern, erstellen Sie eine „ist ein“-Beziehung. Das heißt, Ihre Kindklasse „ist eine“ Elternklasse mit weiteren Funktionalitäten. Wir könnten beispielsweise eine ZFiA_Author-Klasse erstellen, die ZFiA_Person erweitert, was bedeutet, dass ein Autor eine Person ist. Dies sehen Sie in Listing B.4. Listing B.4 ZFiA_Person erweitern, um ZFiA_Author zu erstellen class ZFiA_Author extends ZFiA_Person { protected $_title;
Deklariert eine Variable, die
Mit extends wird Funktionalität vererbt
nur in ZFiA_Author verfügbar ist
public function setTitle($title) { $this->_title = $title; }
Definiert Elementmethode
}
Die Klasse ZFiA_Author erbt alle Eigenschaften und Methoden der Klasse ZFiA_Person. Sie kann auch eigene Eigenschaften n und Methoden o haben. Sie können die Methoden durch das Erstellen neuer Methoden in der Kindklasse mit dem gleichen Namen überschreiben oder verwenden die Methoden der Elternklasse einfach durch Aufruf. Sie müssen sie nicht neu definieren. Außerdem kann das Schlüsselwort final der Deklaration einer Funktion neben dem Schlüsselwort für die Sichtbarkeit vorangestellt werden, um zu verhindern, dass Kindklassen diese Methode überschreiben. Man kann auch eine Klasse erstellen, die allgemeine Funktionalitäten für eine Serie von Kindklassen implementiert, selbst jedoch nicht instanziiert werden kann. Abstrakte Klassen sind dazu gedacht, dieses Problem zu lösen.
B.1.3 Abstrakte Klassen und Interfaces Zwei neue Features von PHP5 sind Interfaces und abstrakte Klassen. Dabei handelt es sich um spezielle, zusammen mit Klassen eingesetzte Konstrukte, damit der Entwickler einer Library festlegen kann, wie Konsumenten, die mit dieser Library arbeiten, mit dem Code arbeiten sollen.
403
B Objektorientiertes PHP B.1.3.1
Abstrakte Klassen
Manche Klassen werden nur deswegen erstellt, damit sie von anderen Klassen erweitert werden. Diese bezeichnet man als abstrakte Klassen, und sie dienen sozusagen als grobe Implementierungen eines Konzepts, das dann erst von den Kindklassen ausgeschmückt wird. Abstrakte Klassen enthalten eigene Elementvariablen und Methoden dazu noch Deklarationen von Methoden, die von den konkreten Kindklassen definiert werden müssen. Wir können beispielsweise ZFiA_Person zu einer abstrakten Klasse machen, für die wir bestimmen, dass alle Personen einen Vor- und einen Nachnamen haben werden, doch wie der formale Name präsentiert wird, bleibt den Kindklassen überlassen (siehe Listing B.5). Listing B.5 Eine abstrakte Klasse enthält undefinierte Funktionen. abstract class ZFiA_Person { protected $_firstName; protected $_lastName;
Deklariert eine abstrakte Klasse
public __construct($firstName, $lastName) { $this->_firstName = $firstName; $this->_lastName = $lastName; } abstract public function fullName();
Definiert eine Standardelementfunktion
Definiert eine abstrakte Funktionssignatur
}
Weil ZFiA_Person nun als abstrakte Klasse deklariert ist, kann sie nicht mehr länger direkt über new instanziiert werden; sie muss von der instanziierten Kindklasse erweitert werden. Die Methode fullName() wird als abstrakt definiert, also müssen alle Kindklassen (sofern sie nicht selbst als abstract deklariert wurden) eine Implementierung dieser Methode anbieten. Wir können eine Implementierung von ZFiA_Author anbieten (siehe Listing B.6). Listing B.6 Erstellen einer konkreten aus einer abstrakten Klasse class ZFiA_Author extends ZFiA_Person { public function fullName() { $name = $this->_firstName . ' ' . $this->lastName; $name .= ', Author'; return $name; } }
Definiert konkrete Implementierung von fullName()
Wie Sie sehen können, stellt die Klasse ZFiA_Author eine spezifische Implementierung von fullName() bereit, die zu keiner anderen Kindklasse von ZFiA_Person passt, weil sie das Wort „Author“ an den Personennamen anhängt.
404
B.1 Objektorientierung in PHP Eine Einschränkung des PHP-Objekt-Models ist, dass eine Klasse nur von einer einzigen Elternklasse erben kann. Es gibt dafür eine Menge guter Gründe, doch das bedeutet, dass ein anderer Mechanismus erforderlich ist, damit eine Klasse zu mehr als einem „Template“ konform sein kann. Das erreicht man über Interfaces (auf deutsch „Schnittstellen“). B.1.3.2
Interfaces
Schnittstellen (Interfaces) definieren eine Vorlage (Template) für Klassen, die das Interface implementieren. Das Interface ist eine API oder ein Kontrakt, den die Klasse erfüllen muss. Praktisch ausgedrückt heißt das, dass ein Interface eine Liste mit Methoden ist, die durch Implementierung von Klassen definiert werden muss. Mit Interfaces kann eine Klasse von einer Methode genutzt werden, die davon abhängig ist, dass die Klasse eine bestimmte Funktionalität implementiert. Wenn wir aus ZFiA_Person ein Interface machen müssten, dann würde man es so deklarieren wie in Listing B.7. Listing B.7 Ein Interface, das die Methoden festlegt, die in der Klasse definiert werden müssen interface ZFiA_Person { public function fullName(); }
Deklariert zu implementierende Methode
class ZFiA_Author implements ZFiA_Person { protected $_firstName; protected $_lastName; public function __construct($firstName, $lastName) { $this->_firstName = $firstName; $this->_lastName = $lastName; } public function fullName() { return $this->_firstName . ' ' . $this->_lastName; }
Definiert Implementierung der Methode des Interfaces
}
Wenn die Methode fullName nicht in ZFiA_Author definiert ist, wird das zu einem schwerwiegenden Fehler führen. Weil es sich bei PHP nicht um eine statisch typisierte Sprache handelt, sind Interfaces praktische Mechanismen für den Programmierer, die ihm dabei helfen, logische Fehler zu verhindern, wie sie vielleicht in Type Hints spezifiziert sind. Durch Type Hinting erfährt der PHP-Interpreter von den Parametern, die an eine Funktion oder Methode übergeben wurden. Schauen Sie sich einmal die Funktion in Listing B.8 an.
405
B Objektorientiertes PHP Listing B.8 Informationen über Funktionsparameter mit Type Hinting function displayPerson(ZFiA_Person $person) { echo $person->fullName(); }
Definiert Type Hinting in
einer Funktionsdeklaration
Um einen Type Hint zu erstellen, muss nur der Typ des Funktionsparameters eingebunden werden – das ist in diesem Fall ZFiA_Person n. Wenn wir versuchen, dieser Funktion eine andere Objektart zu übergeben, bekommen wir folgende Fehlermeldung: Catchable fatal error: Argument 1 passed to displayPerson() must be an instance of ZFiA_Person, string given, called in type_hinting.php on line 18 and defined in type_hinting.php on line 11
Es sollte offensichtlich sein, dass Interfaces beim Programmieren mit PHP nicht erforderlich sind, weil PHP keine statisch typisierte Sprache ist und in der Mehrheit aller Fälle das Richtige machen wird. Der Hauptnutznießer der Interfaces ist der Programmierer, weil sich dadurch der Code selbst dokumentiert. Ein weiteres Feature von PHP5, mit dem auch das Zend Framework Programmierern das Leben erleichtert, ist das Überladen anhand von sogenannten „magischen“ Methoden. Dabei handelt es sich um spezielle Methoden, die PHP bei Bedarf automatisch aufruft, durch die der Programmierer der Klasse eine aufgeräumtere Schnittstelle bieten kann.
B.1.4 Magische Methoden Magische Methoden sind spezielle Methoden, die von PHP unter bestimmten Umständen verwendet werden. Jede Methode, deren Name mit einem „__“ (doppelter Unterstrich) beginnt, wird von der PHP-Sprache reserviert. Also wird allen magischen Methoden ein __ vorangestellt. Die häufigste Methode ist der Konstruktor __construct, den wir uns in Abschnitt B.1.1 angeschaut haben. Tabelle B.1 zeigt alle magischen Methoden, die Sie in Ihrer Klasse verwenden können. Tabelle B.1 Die magischen Methoden von PHP5 werden von PHP bei Bedarf aufgerufen.
406
Methodenname
Prototyp
Wird aufgerufen …
__construct()
void __construct()
… wenn das Objekt instanziiert wird.
__destruct()
void __destruct()
… wenn das Objekt gelöscht und aus dem Speicher entfernt wird.
__call()
mixed __call(string $name, array $argumente)
… wenn die Elementfunktion nicht existiert. Wird zur Implementierung des Methoden-Handlings eingesetzt, das vom Funktionsnamen abhängig ist.
__get()
mixed __get (string $name)
… wenn die Elementvariable beim Auslesen nicht existiert.
B.1 Objektorientierung in PHP Methodenname
Prototyp
Wird aufgerufen …
__set()
void __set (string $name, mixed $wert)
… wenn die Elementvariable beim Einrichten nicht existiert.
__isset()
bool __isset (string $name)
… wenn die Elementvariable beim Test, ob sie vorhanden ist, nicht existiert.
__unset()
void __unset (string $name)
… wenn die Elementvariable beim Löschen nicht existiert.
__sleep()
void __sleep()
… wenn das Objekt serialisiert werden soll. Wird benutzt, um anhängige Daten zu committen, die das Objekt eventuell enthält.
__wakeup()
void __wakeup()
… wenn das Objekt deserialisiert wurde. Wird eingesetzt, um erneut die Verbindung mit externen Ressourcen (wie z. B. einer Datenbank) aufzubauen.
__toString()
void __toString()
… wenn das Objekt in einen String konvertiert wird. Wird verwendet, um auf echo() etwas Vernünftiges auszugeben.
__set_state()
static void __set_state (array $eigenschaften)
… wenn für das Objekt var_export() aufgerufen wurde. Wird benutzt, um die Neuerstellung eines Objekts anhand von eval() zu ermöglichen.
__clone()
void __clone()
… wenn das Objekt kopiert wird. Normalerweise verwendet, um zu gewährleisten, dass die Kopie des Objekts sich um Referenzen kümmert.
__autoload()
void __autoload ($klassenname)
… wenn die Klasse bei der Instanziierung nicht gefunden wird. Wird verwendet, um die Klasse über include() zu laden, indem der Name der Klasse einer Datei auf der Festplatte zugeordnet wird.
Schauen wir uns mal eine Verwendung von __set() und __get() in Zend_Config an. Zend_Config speichert alle Variablen innerhalb einer geschützten Elementvariable namens $data und verwendet die magischen Methoden __set() und __get(), um den öffentlichen Zugriff auf die Information zu ermöglichen (siehe Listing B.9).
407
B Objektorientiertes PHP Listing B.9 So verwendet Zend_Config __get() und __set(). class Zend_Config implements Countable, Iterator { protected $_data;
Deklariert zu implementierendes Interface
Deklariert Elementvariable
public function get($name, $default = null) Definiert Methode, { um Datenwert $result = $default; if (array_key_exists($name, $this->_data)) { auszulesen $result = $this->_data[$name]; } return $result; } Definiert magische Methode,
public function __get($name) { return $this->get($name); }
die get() nutzt
Spezielle Bedingungen für
das Setzen einer Variable public function __set($name, $value) { if ($this->_allowModifications) { if (is_array($value)) { $this->_data[$name] = new Zend_Config($value, true); } else { $this->_data[$name] = $value; } $this->_count = count($this->_data); } else { throw new Zend_Config_Exception('Zend_Config is read only'); } } }
Wie Sie Listing B.9 entnehmen können, verfügt Zend_Config über die öffentliche Methode get(), mit der eine Variable ausgelesen wird, und gibt einen Standardwert zurück, falls die Variable nicht gesetzt wurde n. Die magische Methode __get() verwendet die bereits geschriebene Methode get()o und erlaubt so den Zugriff auf eine Variable, als wäre sie eine native Elementvariable. Zum Beispiel ist der Aufruf von $adminEmail = $config->adminEmail;
identisch mit diesem Aufruf: $adminEmail = $config->get('adminEmail');
außer dass die erste Möglichkeit sauberer und einfacher zu merken ist! Entsprechend wird __set() zum Setzen einer Variable verwendet p. Diese Methode implementiert eine private Business-Logik, die dafür sorgt, dass eine Variable nur dann gesetzt werden darf, falls beim Objekt die Variable __allowModifications gesetzt ist. Wenn das Objekt mit Nur lesen erstellt wurde, wird stattdessen eine Exception geworfen.
408
s
B.2 Die Standard PHP Library Damit schließen wir unsere kurze Spritztour durch die wichtigsten Objekte in PHP ab. Weiterführende Informationen finden Sie in den Kapiteln 2 bis 6 in PHP in Action von Reiersøl, Baker und Shiflett. Ein weiterer Bereich von PHP5, der im Zend Framework verwendet wird, ist die Standard PHP Library (SPL).
B.2 Die Standard PHP Library Die SPL ist eine Library mit praktischen Schnittstellen, um den Datenzugriff zu vereinfachen. Sie enthält die folgenden Schnittstellenkategorien:
Iteratoren: Damit können Sie Collections, Dateien, Verzeichnisse oder XML anhand von Schleifen durchlaufen.
Zugriff auf Arrays: Damit verhalten sich Objekte wie Arrays. Counting: Ermöglicht Objekten, mit count() zu arbeiten. Observer: Implementiert das Software-Designpattern Observer. Weil dies eine Kurztour durch die SPL ist, werden wir uns nur mit der Iteration, dem Arrayzugriff und dem Counting beschäftigen, weil wir das Observer-Designpattern bereits in Kapitel 9 angeschaut haben. Wir beginnen mit den Iteratoren, weil sie das Haupteinsatzgebiet von SPL sind.
B.2.1 Die Arbeit mit Iteratoren Iteratoren sind Objekte, die Strukturen wie ein Array, ein Datenbankergebnis oder eine Verzeichnisauflistung anhand einer Schleife durchlaufen. Sie brauchen einen Iterator für jede Art von Struktur, durch die iteriert werden soll, und die am häufigsten verwendeten wie Arrays und Verzeichnisse sind direkt in PHP und SPL integriert. Eine übliche Situation ist, ein Verzeichnis zu lesen, und das wird wie in Listing B.10 gezeigt anhand des DirectoryIterator-Objekts von SPL erledigt. Listing B.10 Durch eine Verzeichnisauflistung anhand von DirectoryIterator iterieren $dirList = new DirectoryIterator('/'); foreach ($dirList as $item) { Verwendet Iterator echo $item . "\n"; für Schleife in foreach() }
Erstellt Iterator
Wie Sie sehen, ist die Nutzung eines Iterators so einfach wie eine foreach()-Schleife, und das ist auch Sinn und Zweck der Sache! Durch Iteratoren wird der Zugriff auf DatenCollections sehr vereinfacht, und zusammen mit selbst erstellten Klassen kommen sie erst recht zur Geltung. Das sieht man im Zend Framework in der Klasse Zend_Db_Table_ Rowset_Abstract, wo Sie mit Iteratoren alle Resultate aus einer Abfrage durchlaufen können.
409
B Objektorientiertes PHP Schauen wir uns ein paar Highlights der Implementierung an. Zuerst nutzen wir Zend_Db_Table_Rowset (siehe Listing B.11). Listing B.11 Die Iteration mit Zend_Db_Table_Rowset class Users extends Zend_Db_Table {}
Definiert GatewayKlasse zur UsersDatenbanktabelle
Holt Rowset
$users = new Users(); $userRowset = $users->fetchAll(); foreach ($userRowset as $user) { echo $user->name . "\n"; }
Iteriert durch Rowset-Klasse
Wie zu erwarten war, ist auch die Verwendung eines Rowsets so einfach wie bei den Arrays. Möglich wird das durch die Implementierung des SPL-Interface Iterator durch Zend_Db_Table_Rowset_Abstract (siehe Listing B.12). Listing B.12 Implementierung eines Iterators mit Zend_Db_Table_Rowset_Abstract abstract class Zend_Db_Table_Rowset_Abstract implements Iterator, Countable { public function current() { if ($this->valid() === false) { return null; }
Methode des Iterators
Gibt am Ende null zurück, um aus each() oder while() zurückzukehren
return $this->_rows[$this->_pointer]; } public function key() { return $this->_pointer; } public function next()
Definiert current()-
Definiert key()-
Methode des Iterators
Definiert next()-
Methode des Iterators
{ ++$this->_pointer; } public function rewind() { $this->_pointer = 0; }
Definiert rewind()Methode des Iterators Definiert valid()-
Methode des Iterators public function valid() { return $this->_pointer < $this->_count; } }
Zend_Db_Table_Rowset_Abstract hält seine Daten in einem Array namens $_rows vor und zählt in $_count, wie viele Elemente sich im Array befinden. Wir müssen nachhalten,
410
B.2 Die Standard PHP Library wo wir uns im Array befinden, während die foreach()-Methode fortschreitet. Dafür nehmen wir die Elementvariable $_pointer. Um Iterator zu implementieren, muss eine Klasse fünf Methoden bereitstellen: current()n, key()o, next()p, rewind()q und valid()r. Diese Methoden manipulieren einfach wie erforderlich die $_pointerVariable und geben die korrekten Daten in current() zurück (siehe Listing B.12). Eine Einschränkung von Iterator ist, dass es auf den Vorwärtsdurchlauf der Daten begrenzt ist und keinen beliebigen Zugriff erlaubt. Das heißt, Folgendes geht mit einer Instanz von Zend_Db_Table_Rowset_Abstract nicht: $aUser = $userRowset[4];
Das liegt im Design begründet, weil die der Klasse zugrundeliegende Technologie eine Datenbank sein könnte, die möglicherweise keinen beliebigen Zugriff auf einen Datensatz erlaubt. Bei anderen Objekttypen könnte der beliebige Zugriff allerdings erforderlich sein, und für diese Fälle ist das Interface ArrayAccess gedacht.
B.2.2 Die Arbeit mit ArrayAccess und Countable Das Interface ArrayAccess erlaubt, dass sich eine Klasse wie ein Array verhält. Das bedeutet, dass der Benutzer einer Klasse beliebig auf jedes Element innerhalb der Collection über die folgende []-Notation zugreifen kann: $element = $objectArray[4];
Genauso wie bei Iterator wird das durch die Nutzung eines Methoden-Sets erreicht, das von der Klasse implementiert werden muss. In diesem Fall sind die Methoden offsetExists(), offsetGet(), offsetSet() und offsetUnset(). Diese Namen sind sprechen für sich, und damit können verschiedene Array-Operationen ausgeführt werden (siehe Listing B.13). Listing B.13 Array-Operationen mit einer ArrayAccess-fähigen Klasse if(isset($rob['firstName']) { echo $rob['firstName']; $rob['firstName'] = 'R.'; } unset($rob['firstName']);
Ruft offsetGet() auf Ruft offsetSet() auf
Ruft offsetExists() auf
Ruft offsetUnset() auf
Die andere auf Arrays basierende Funktionalität, die man praktischerweise im Kontext eines Objekts implementieren sollte, ist die Funktion count(). Das geschieht über das Interface Countable, welches nur die einzige Methode count() enthält. Wie Sie sicher erwartet haben, muss diese Funktion die Anzahl der Elemente in der Collection zurückgeben, und schon haben wir alle Tools, damit sich eine Klasse wie ein Array verhält. Die SPL hat viele andere Iteratoren und andere Interfaces, die für kompliziertere Anforderungen zur Verfügung stehen. All diese dokumentiert das exzellente php.net-Manual.
411
B Objektorientiertes PHP
B.3 PHP4 Mittlerweise sollten alle nur noch mit PHP5 arbeiten, weil es mehr als vier Jahre alt ist und PHP4 nicht mehr länger unterstützt wird. Wenn Sie Ihre ersten Schritte mit dem Zend Framework in PHP5 unternehmen, sollten Sie den ausgezeichneten Migrationsleitfaden lesen, der auf der PHP-Website unter http://www.php.net/manual/en/migration5.php bereitsteht. Durch die Annahme von PHP5 hat die PHP-Szene Software-Designpattern kennengelernt, die beschreiben, wie man übliche Probleme auf eine sprachagnostische Weise löst. Designpattern werden im gesamten Core des Zend Frameworks verwendet. Somit wollen wir hier einige häufig verwendete vorstellen.
B.4 Software-Designpatterns Wer Software produziert, stößt schon bald auf Probleme, die solchen ähneln, die ihm schon mal untergekommen sind und den gleichen Lösungsansatz erfordern. Das kommt in der gesamten Softwareentwicklung vor, und somit gibt es längst Best Practices bei den Lösungen bestimmter Problemklassen. Damit diese Lösungsansätze einfacher zu diskutieren sind, wurde der Begriff Designpattern (auf deutsch: Entwurfsmuster) geprägt. Mittlerweile stehen ganze Kataloge von Designpattern zur Verfügung, mit denen man Softwaredesignprobleme lösen kann. Das Wichtigste bei Designpattern ist, dass es dabei nicht um den Code geht, sondern es sich um Richtlinien handelt, wie man das Problem löst. In diesem Abschnitt schauen wir uns die Designpattern Singleton und Registry an. Beide werden im Core des Zend Frameworks verwendet.
B.4.1 Das Singleton-Designpattern Dieses Designpattern ist sehr einfach und soll sicherstellen, dass nur jeweils eine Instanz eines Objekts existiert. Es wird von der Zend_Controller_Front-Klasse des Zend Frameworks eingesetzt, um zu gewährleisten, dass für die Dauer der Anfrage nur einen FrontController gibt. Listing B.14 zeigt den relevanten Code zur Implementierung des Singleton-Designpatterns in Zend_Controller_Front.
412
B.4 Software-Designpatterns Listing B.14 Das von Zend_Controller_Front implementierte Singleton-Designpattern
Deklariert statische Variable,
class Zend_Controller_Front { protected static $_instance = null;
die die eine Instanz dieses Objekts enthält
protected function __construct() {
Definiert geschützten Konstruktor
$this->_plugins = new Zend_Controller_Plugin_Broker(); } public static function getInstance() { if (null === self::$_instance) { self::$_instance = new self(); }
Definiert Methode,
die Erstellung erlaubt
Erstellt eine Instanz,
falls noch keine erstellt wurde
return self::$_instance; } }
Gibt Instanz an User zurück
Zum Singleton-Puzzle gehören drei Teile, die von Zend_Controller_Front implementiert werden, und die meisten anderen Implementierungen sind ähnlich. Die Instanz des FrontControllers wird in einer statischen Variable gespeichert n. Diese ist geschützt, damit von außerhalb dieser Klasse nicht direkt zugegriffen werden kann. Um zu verhindern, dass das Schlüsselwort new verwendet wird, ist der Konstruktor protected o (geschützt). Also bleibt als einzige Möglichkeit, eine Instanz dieser Klasse von außerhalb ihrer Kinder aufzurufen, die statische Methode getInstance() p. getInstance() ist statisch, weil wir keine Instanz der Klasse haben, wenn wir sie aufrufen; sie gibt uns eine Instanz zurück. Innerhalb von getInstance() instanziieren wir die Klasse nur einmal q, anderenfalls geben wir nur die bereits erstellte Instanz zurück r. Beachten Sie, dass bei Zend_ Controller_Front die Kindklassen ihren eigenen Konstruktor implementieren können. Auch wird der Konstruktor häufig private gemacht, was man in Zend_Search_Lucene_ Document_Html und anderen Klassen innerhalb des Zend Frameworks sehen kann. Das Endergebnis ist, dass der Code für den Zugriff auf den Front-Controller wie folgt lautet: $frontController = Zend_Controller_Front::getInstance();
Dieser Code funktioniert überall, wo wir ihn brauchen, und er erstellt uns einen FrontController, wenn wir noch keinen haben. Anderenfalls gibt er eine Referenz auf den bereits erstellten Controller zurück. Das Singleton-Designpattern ist zwar einfach zu implementieren, aber es hat einen großen Nachteil: Durch die Hintertür ist es eine globale Variable. Weil das Singleton eine statische Methode hat, die einen Verweis auf das Objekt zurückgibt, kann es von überall her aufgerufen werden, und wenn es unklug verwendet wird, könnte es für eine Kopplung zwischen grundverschiedenen Abschnitten eines Programms sorgen. Singletons sind auch schwer zu testen, weil Sie spezielle Methoden brauchen, um den Status des Objekts zurückzusetzen.
413
B Objektorientiertes PHP Anmerkung
Kopplung (coupling) nennt man die Situation, wenn zwei Code-Abschnitte auf irgendeine Weise verlinkt sind. Durch eine größere Kopplung wird es schwerer, Bugs nachzuverfolgen, weil es mehrere Stellen gibt, von denen aus der Code möglicherweise geändert wurde.
Dementsprechend sollte man sorgfältig aufpassen, wenn man eine Klasse als Singleton erstellt, und auch wenn man sich dafür entscheidet, die getInstance()-Methode irgendwo anders im Code zu verwenden. Normalerweise ist es besser, ein Objekt herumzureichen, denn der Zugriff darauf ist leichter zu steuern, als auf das Singleton direkt zuzugreifen. Ein Beispiel dafür, wo wir das innerhalb der Action-Controller-Klasse machen können, ist innerhalb der Action-Controller-Klasse: Zend_Controller_Action enthält eine getRequest()-Methode, die das Request-Objekt zurückgibt, und diese Methode sollte man gegenüber Zend_Controller_Front::getInstance()->getRequest() präferieren. Es ist nicht unüblich, auf allgemein verwendete Informationen von mehreren Stellen (z. B. Konfigurationsdaten) aus zuzugreifen. In dem Fall können wir mit dem RegistryDesignpattern gewährleisten, dass die Verwaltung solcher Daten ebenfalls gut funktioniert.
B.4.2 Das Registry-Designpattern Mit diesem Designpattern können wir ein beliebiges Objekt als Singleton behandeln, und diese Objekte können dann zentral verwaltet werden. Die größte Verbesserung gegenüber dem Singleton ist, dass man falls erforderlich zwei Instanzen eines Objekts haben kann. Ein typisches Szenario ist ein Datenbankverbindungsobjekt. Normalerweise bauen Sie für die Dauer der Anfrage eine Verbindung zu einer Datenbank auf. Es wäre also verführerisch, aus dem Objekt ein Singleton zu machen. Doch gelegentlich wollen Sie sich vielleicht auch mit einer zweiten Datenbank verbinden (beispielsweise für Im- oder Export), und dafür brauchen Sie zwei Instanzen des Datenbankverbindungsobjekts. Mit dem Singleton geht das nicht, doch mit der Registry bekommen Sie die Vorteile eines leicht zugreifbaren Objekts ohne irgendwelche Nachteile. Das Problem kommt so häufig vor, dass das Zend Framework die Klasse Zend_Registry enthält, die das Registry-Designpattern implementiert. Für den einfachen Zugriff gibt es zwei Hauptfunktionen, die die grundlegende API von Zend_Registry bilden. Wie sie verwendet werden, zeigen die Listings B.15 und B.16. Listing B.15 Ein Objekt in Zend_Registry speichern // bootstrap class $config = $this->loadConfig(); Zend_Registry::set('config', $config);
Speichert das Objekt in den Registry-Objekten
Um ein Objekt in der Registry zu speichern, arbeitet man mit der set()-Methode. Wie vorherzusehen war, liest man dann mit der get()-Methode ein Objekt von einer anderen Stelle im Code aus.
414
B.4 Software-Designpatterns Listing B.16 Ein Objekt aus der Zend_Registry auslesen // bookstrap class $config = Zend_Registry::get('config');
Liest Objekt über Schlüsselnamen aus: „config“
Die einzige, bei Zend_Registry zu beachtende Komplikation ist: Sie müssen gewährleisten, dass Sie einen eindeutigen Schlüssel angeben, wenn Sie das Objekt in die Registry speichern. Anderenfalls werden Sie das bereits mit dem gleichen Namen gespeicherte Objekt überschreiben. Mal abgesehen davon ist es einfacher, eine Registry zu verwenden, als eine Klasse so zu modifizieren, dass daraus ein Singleton wird. Die zentralen Komponenten der Implementierung stehen in Listing B.17. Schauen wir uns eine nach der anderen an. Listing B.17 Das von Zend_Registry implementierte Registry-Designpattern class Zend_Registry extends ArrayObject { public static function getInstance() { if (self::$_registry === null) { self::init(); }
Verwendet intern
ein Singleton-Objekt
return self::$_registry; } public static function set($index, $value) { $instance = self::getInstance(); $instance->offsetSet($index, $value); }
Setzt Element über
ArrayObjekt-Interface
public static function get($index) { $instance = self::getInstance(); if (!$instance->offsetExists($index)) { require_once 'Zend/Exception.php'; throw new Zend_Exception("No entry is registered for key '$index'"); } return $instance->offsetGet($index);
Prüft, ob Schlüssel existiert
Liest Daten aus
und gibt sie zurück
} }
Intern arbeitet Zend_Registry mit einer Singleton-ähnlichen Methode getInstance(), um eine Instanz von sich selbst auszulesen n. Das wird sowohl in get()als auch in set() verwendet und gewährleistet, dass beide Methoden die gleiche Registry nehmen, wenn Sie Elemente setzen und auslesen. Zend_Registry erweitert ArrayObject, was bedeutet, dass Sie eine Instanz davon so behandeln können, als wäre es ein Array. Sie können also direkt darüber iterieren und Elemente holen, als wären es Array-Schlüssel. ArrayObject wird von der SPL bereitgestellt und implementiert die für die Schnittstelle ArrayAccess erforderlichen Methoden einschließlich offsetGet(), offsetSet() und offsetExists(). Das
415
B Objektorientiertes PHP erspart einem, diese Methoden selbst schreiben zu müssen, obwohl sie überschrieben werden können, falls selbst erstellte Funktionalitäten erforderlich sind. Die statischen Methoden set() und get() müssen die gleichen Actions ausführen. Also wird offsetSet() von set() aufgerufen, um ein Item in der Registry zu setzen o, und mit offsetGet() wird ein Item aus der Registry innerhalb von get() ausgelesen p. Beachten Sie, dass der Code durch Verwendung von offsetGet() und offsetSet() nichts vom zugrundeliegenden Mechanismus zur Speicherung der Items mitbekommt. Durch diese Zukunftsabsicherung kann der Speicherungsmechanismus bei Bedarf geändert werden, ohne dass dieser Code aktualisiert werden muss. Wir haben uns zwei der üblichen Designpattern für Webapplikationen angeschaut, und wie bereits angemerkt, gibt es noch viele andere. Zend Framework implementiert ebenfalls einige mehr und ist somit eine gute Code-Basis, um diese Designpattern zu studieren. Hier folgen einige der anderen verwendeten Designpattern:
MVC in der Funktionsfamilie von Zend_Controller Table-Data-Gateway in Zend_Db_Table Row-Data-Gateway in Zend_Db_Row Strategy in Zend_Layout_Controller_Action_Helper_Layout Observer in Zend_XmlRpc_Server_Fault Es sind noch eine Vielzahl von Designpattern im Umlauf, und wir empfehlen die Lektüre von Patterns für Enterprise Application-Architekturen von Martin Fowler und php|architect’s Guide to PHP Design Patterns von Jason Sweat für weitere Informationen über die häufig eingesetzten weborientierten Patterns. Auch PHP in Action von Reiersøl, Baker und Shiflett führt gut lesbar in die feineren Details der Arbeit mit Designpattern und PHP ein.
B.5 Zusammenfassung Das Zend Framework ist ein modernes PHP5-Framework und nutzt von daher alle neuen Goodies, die PHP5 anbietet. Die Kenntnis der zentralen Features des Objektmodells von PHP5 ist für das Schreiben von Applikationen mit dem Zend Framework unabdingbar. Jeder Teil des Frameworks ist in objektorientiertem PHP geschrieben, und die SPLInterfaces wie ArrayAccess und dessen Geschwister wie ArrayObject, Countable und Iterator werden umsichtig eingesetzt. Die SPL liefert Interfaces zu Klassen, durch die sie sich mehr wie native PHP-Arrays verhalten. Software-Designpattern sind ein wichtiger Teil des modernen Webdevelopments. Wir haben uns deren Funktionsweise angeschaut sowie den Einsatz der Designpattern Singleton und Registry im Zend Framework. Dieses Kapitel bot einen Überblick und weitergehende Informationen. Wir können nur nachdrücklich empfehlen, sich die anderen angegebenen Bücher vorzunehmen, weil diese Themen darin erschöpfender behandelt werden. Nach dem hier zusammengetragenen Wissen sind Sie also gut aufgestellt, um sich kopfüber ins Zend Framework und die Hauptkapitel hineinzustürzen.
416
C
Tipps und Tricks
Die Themen dieses Anhangs
Module mit MVC-Komponenten des Zend Frameworks verwenden Die Auswirkungen von Groß/Kleinschreibung auf Controller-Klassen und ActionFunktionen
URL-Routing-Komponenten des Zend Frameworks Ihre Applikation mit Zend_Debug, Zend_Log und Zend_Db_Profiler verstehen Hier schauen wir uns nun ein paar anspruchsvollere Verwendungsmöglichkeiten einiger zentraler Zend Framework-Komponenten an. Innerhalb der MVC-Komponenten wird es darum gehen, wie man anhand von Modulen den Code noch weiter separiert, und wir untersuchen, wie der Dispatcher mit Actions und Controllern mit Großbuchstaben umgeht. Dann beschäftigen wir uns damit, wie die statischen und Regex-Routing-Klassen flexiblere und effizientere URLs ermöglichen. Wenn eine Webapplikation wächst, wird es immer wichtiger, die ablaufenden Prozesse zu protokollieren. Mit Zend_Log erfahren Sie, was in der Applikation passiert. Wir schauen uns die Vorteile von Zend_Debug an, wenn Sie beim Debuggen des Codes mal auf die Schnelle einen Check machen wollen. Schließlich geht es um den Zend_Db_Profiler, der Ihnen genau zeigt, welche SQLAnweisungen gestartet werden und wie lange jede Anweisung braucht. Das ist sehr praktisch, wenn Sie den Ladevorgang Ihrer Seite optimieren wollen. Steigen wir also gleich bei den Modulen im MVC-System ein.
417
C Tipps und Tricks
C.1 Tipps und Tricks für MVC In diesem Abschnitt geht es um die Features des MVC-Systems, die im restlichen Buch noch nicht angesprochen wurden, entweder weil sie anspruchsvoller sind oder in den meisten Projekten wahrscheinlich nicht so häufig eingesetzt werden.
C.1.1 Module Wenn Sie die Online-Dokumentation über Zend_Controller sorgfältig durchlesen, erfahren Sie, dass das MVC-System des Zend Frameworks mit Modulen arbeitet. Durch Module wird eine weitere Schicht der Separation für die Applikation eingeführt. Damit trennt man normalerweise verschiedene Bereiche einer Applikation, um die Wiederverwendung von MVC-Miniapplikationen zu fördern. Eine einfache CMS-Applikation kann die beiden Module Blog und Pages verwenden, jede mit einem eigenen Verzeichnis für Controller, Model und Views. Abbildung C.1 zeigt, wie die Verzeichnisse in dieser CMS-Applikation aussehen würden.
Abbildung C.1 Die Verzeichnisstruktur einer modularen Zend Framework-Applikation kann einen eigenen Set an MVC-Unterverzeichnissen haben.
Jedes Modul hat sein eigenes controllers-Verzeichnis, in dem die Controller-Klassen enthalten sind. Die Klassen verwenden die Namenskonvention {Modul-Name}_{ControllerName}Controller und werden in der Datei application/modules/{Modul-Name}/controllers/ {Controller-Name}.php gespeichert. Beispielsweise wird der Index-Controller der SeitenModule in application/modules/pages/controllers/IndexController.php gespeichert und heißt Pages_IndexController. Es gibt auch das Standardset an MVC-Verzeichnissen im Applikationsordner selbst. Das sind die Standard-Module, die sich genauso verhalten wie bei einer nicht modularisierten
418
C.1 Tipps und Tricks für MVC Applikation. Das heißt, den Controllern im Standard-Modul ist der Modulname nicht als Präfix vorangestellt. Der Index-Controller im Standard-Modul heißt also einfach IndexController und wird in application/controllers/IndexController.php gespeichert. Standardmäßig wird, wenn alle zusätzlichen Module beim Front-Controller registriert sind, der Standard-Router das URL-Schema {Basis-URL}/{Modul-Name}/{Controller-Name}/ {Action-Name} verwenden. Beispielsweise wird http://example.com/pages/index/view die View-Action im Standard-Controller des Pages-Moduls ausführen (die viewAction()Methode innerhalb der Klasse Pages_IndexController). Schauen wir uns an, wie man mit dem Front-Controller Module registriert und dabei den Bootstrap-Code von Places verwendet. In Listing 3.1 aus Kapitel 3 wurde die BootstrapKlasse eingeführt und das Verzeichnis des Controllers eingerichtet (siehe Listing C.1). Listing C.1 Front-Controller-Setup für Places aus Kapitel 3 public function runApp() { // setup front controller $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(false); Richtet Controllers$frontController->setControllerDirectory( Verzeichnis ein ROOT_DIR . '/application/controllers');
// ...
Die wichtige Zeile ist der Aufruf von setControllerDirectory()n, durch den der Front-Controller erfährt, wo die Controller-Klassen zu finden sind. Um auch die aus den Modulen einzubinden, rufen wir addControllerDirectory() des Front-Controllers der Reihe nach wie folgt für jedes Modul auf: $frontController->addControllerDirectory( ROOT_DIR . '/application/modules/blog, 'blog'); $frontController->addControllerDirectory( ROOT_DIR . '/application/modules/pages, 'pages');
Wenn Sie viele Module haben, wird das sehr langatmig. Eine Alternative ist der Einsatz von addModuleDirectory(), das durch ein Verzeichnis iteriert und alle ControllersVerzeichnisse der Unterverzeichnisse für Sie hinzufügt. Dies sehen Sie in Listing C.2. Listing C.2 Module in das Front-Controller-Setup für Places aus Kapitel 3 einfügen public function runApp() { // set up front controller $frontController = Zend_Controller_Front::getInstance(); $frontController->throwExceptions(false); $frontController->setControllerDirectory( ROOT_DIR . '/application/controllers'); $frontController->addModuleDirectory( ROOT_DIR . '/application/modules');
Fügt alle
Module ein
// ...
419
C Tipps und Tricks Wie Sie sehen, müssen wir nur eine einzige Code-Zeile ins Bootstrap einfügen n, damit alle Controller-Verzeichnisse in den Unterverzeichnissen des Module-Verzeichnisses unterstützt werden. Wenn wir addModuleDirectory() nehmen, löst das auch ein potenzielles Wartungsproblem, weil wir uns um diesen Code nie wieder zu kümmern brauchen. Wenn wir addControllerDirectory() genommen hätten, dann müssten wir diesen Code jedes Mal aktualisieren, wenn ein neues Modul in die Applikation eingefügt wird. In der Verwendung funktioniert eine modulare genauso wie eine normale Applikation. Die View-Hilfsklasse url() arbeitet auf die gleiche Weise mit den Modulen. Um einen URL für die Index-Action des Index-Controllers in einem anderen Modul zu erstellen, sieht das View-Skript wie folgt aus: $this->url(array('module'=>'another', controller=>'index', 'action'=>'index'));
Ein doch noch entstehendes Problem ist, wie die Models des Moduls geladen werden. Eine Möglichkeit ist, den PHP-include_path dynamisch zu ändern, sodass sich das korrekte Models-Verzeichnis des Moduls im Pfad befindet. Das erledigt man am besten mit einem Front-Controller-Plug-in (siehe Listing C.3). Um Namenskonflikte zu vermeiden, arbeiten wir mit einem Klassenpräfix, um dem Plug-in einen Namensraum zu geben. Die von uns befolgte Konvention ordnet den Namen der Klasse ihrem Standort auf der Festplatte zu. In diesem Fall heißt die Klasse Places_Controller_Plugin_ModelDirSetup und wird folglich in der Datei library/Places/Controller/ModelDirSetup.php gespeichert. Listing C.3 Den inlcude-Pfad zu den Models des Moduls mit ModelDirSetup setzen class Places_Controller_Plugin_ModelDirSetup extends Zend_Controller_Plugin_Abstract { protected $_baseIncludePath; public function __construct() { $this->_baseIncludePath = get_include_path(); }
C.1 Tipps und Tricks für MVC So wie bei den anderen beiden Plug-ins ActionSetup und ViewSetup wird das Plug-in ModelDirSetup anhand des folgenden Codes in der runApp()-Funktion der BootstrapKlasse geladen: $frontController->registerPlugin(new Places_Controller_Plugin_ModelDirSetup());
Die Klasse speichert den aktuellen PHP- im Konstruktor include_path n, damit wir ihn später nutzen können. Die eigentliche Arbeit macht die preDispatch()-Methode. So können wir das Models-Verzeichnis für jede Action einrichten, was ganz wichtig ist, weil eventuell jede Action in der Dispatch-Schleife zu einem anderen Modul gehört. Wir richten das Module-Verzeichnis durch Aufruf von dirname() im Controller-Verzeichnis ein, das anhand der getControllerDirectory()-Methode des Front-Controllers und durch Einfügen von '/models' o darin bezogen wird. Dann stellen wir dem bereits gespeicherten include_path p das Models-Verzeichnis voran. Damit ist gewährleistet, dass wir keine multiplen Model-Verzeichnisse im include-Pfad haben, wenn jede Action in der DispatchSchleife ausgeführt wird. Umsichtig eingesetzt, kann man mit Modulen eine komplexe Applikation mit vielen Controllern in besser verwaltbare Teile aufteilen. Wenn Sie allerdings auf die Models eines anderen Moduls zugreifen müssen, gibt es im Zend Framework dafür keine direkte Unterstützung, und oft ist es am einfachsten, den vollständigen Pfad in einer require()Anweisung zu nehmen. Schauen wir uns nun an, wie das MVC-System mit den Großbuchstaben in Controller- und Action-Namen umgeht.
C.1.2 Case Sensitivity Router und Dispatcher des Zend Frameworks achten auf Groß- bzw. Kleinschreibung von Action- und Controller-Namen – das bezeichnet man als case sensitive. Das hat seinen guten Grund: Die Funktionen und Methoden von PHP sind nicht case sensitive, doch Dateinamen im Allgemeinen schon. Das bedeutet, dass es Regeln gibt, wann Großbuchstaben oder andere Worttrenner als Controller-Namen verwendet werden müssen, um sicher zu sein, dass die korrekten Klassendateien gefunden werden. Für Action-Namen ist es wichtig, dass das korrekte View-Skript gefunden wird. Die folgenden Abschnitte umreißen die Regeln für Controller- und Action-URLs. C.1.2.1
Worttrennung innerhalb von Controller-URLs
Der erste Buchstabe eines Controller-Klassennamens muss groß geschrieben werden – das wird vom Dispatcher erzwungen. Der Dispatcher konvertiert außerdem bestimmte Worttrennungszeichen in Verzeichnistrenner, wenn der Dateiname für die Klasse bestimmt wird. Tabelle C.1 zeigt die Auswirkungen der verschiedenen Controller-URLs auf den Datei- und Klassennamen des Controllers für einen Controller, den wir hier einmal „tech support“ nennen wollen.
421
C Tipps und Tricks Tabelle C.1 Die Zuordnung von Controller-URLs zu Controller-Klassennamen und -Dateinamen Action-URL
Klassenname des Controllers
Dateiname des Controllers
/techsupport/
TechsupportController
TechsupportController.php
/techSupport/
TechsupportController
TechsupportController.php
/tech-support/
TechSupportController
TechSupportController.php
/tech.support/
TechSupportController
TechSupportController.php
/tech_support/
Tech_SupportController
Tech/SupportController.php
Wie Sie der Tabelle C.1 entnehmen können, werden große Buchstaben in ControllerNamen immer zu Kleinbuchstaben umgewandelt, und nur die Worttrenner Punkt (.) und Bindestrich (-) führen zu einem MixedCase-Klassen- bzw. Dateinamen. In allen Fällen erfolgt eine direkte Zuordnung von Klassen- auf Dateiname. Damit werden die Standardkonventionen des Zend Frameworks befolgt. Wenn also im URL des Controllers ein Unterstrich vorkommt, fungiert er als Verzeichnisseparator auf der Festplatte. Action-Namen haben ähnliche, doch etwas andere Regeln, weil sowohl der Dispatcher als auch der ViewRenderer an der Auflösung einer Action-URL hin zu einer Action-Methode und dann weiter zu der damit verknüpften View-Skript-Datei beteiligt sind. C.1.2.2
Worttrennung innerhalb von Action-URLs
Der Dispatcher erzwingt die Case Sensitivity innerhalb der Namen von Action-Methoden. Das bedeutet, dass er erwartet, dass der Name Ihrer Action-Methode klein geschrieben wird, außer Sie separieren die Wörter im URL mit einem bekannten Wortseparator. Standardmäßig werden nur der Punkt (.) und der Bindestrich (-) als Wortseparatoren erkannt. Wenn Sie Ihre Action also in camelCase-Form benennen (z.B. viewDetails), dann wird die aufgerufene Action die komplett klein geschriebene Methode viewdetailsAction() sein. Der ViewRenderer ordnet nun die erkannten Wortseparatoren und auch eine Veränderung auf Großbuchstaben einem Bindestrich im Dateinamen des View-Skripts zu. Das bedeutet, dass der Action-URL viewDetails dem View-Skript-Dateinamen view-details.phtml zugeordnet wird. Tabelle C.2 zeigt die Auswirkungen unterschiedlicher URL-Wortseparatoren bei den aufgerufenen Actions und View-Skripts. Tabelle C.2 Zuordnung von Action-URLs auf Action-Funktionen und View-Skript-Dateinamen
422
Action-URL
Action-Funktion
View-Skript-Dateiname
/viewdetails
viewdetailsAction()
viewdetails.phtml
/viewDetails
viewdetailsAction()
view-details.phtml
C.1 Tipps und Tricks für MVC Action-URL
Action-Funktion
View-Skript-Dateiname
/view_details
viewdetailsAction()
view-details.phtml
/view-details
viewDetailsAction()
view-details.phtml
/view.details
viewDetailsAction()
view-details.phtml
Wie Sie der Tabelle C.3 entnehmen können, muss man schon ein wenig aufpassen, wenn man mit Wortseparatoren in URLs arbeitet. Wir empfehlen, dass Sie immer einen Bindestrich in Ihren URLs verwenden, wenn Sie mit Wortseparation arbeiten wollen. Das führt zu einfach zu lesenden URLs, Action-Funktionsnamen in camelCase und einem Bindestrichseparator in Ihren View-Skript-Dateinamen, was mit dem Action-Namen im URL konsistent ist. Die Regeln für die Wortseparation in Controller- und Action-Namen folgt einem festgelegten System. Im Allgemeinen ist die Verwendung des Bindestrichs am einfachsten und führt zu URL-Separationen, die auch von Google und anderen Suchmaschinen akzeptiert werden. Also lautet unsere Empfehlung, dass Sie damit arbeiten, wenn Sie Controller- und Action-Namen mit mehreren Wörtern separieren wollen. Schauen wir uns nun an, wie man den Router einsetzt, um Vorteile für Geschwindigkeit und Flexibilität bei der Konvertierung von URLs zu Action-Funktionen zu erzielen.
C.1.3 Routing Als Routing bezeichnet man den Prozess der Konvertierung eines URLs in eine zu startende Action. Standard ist der rewrite-Router in der Zend_Controller_Router_RewriteKlasse, der als Default die von einer Zend Framework-MVC-Applikation verwendete Standard-Route anhängt. Die Standard-Route übersetzt URLs der Form modul/controller/action/variable1/wert1/. Ein URL wie news/index/view/id/6 wird beispielsweise der „view“-Action im „index“Controller des „news“-Moduls zugeordnet, wobei der zusätzliche id-Parameter auf 6 gesetzt ist. Auf den id-Parameter kann im Controller anhand von $this>_getParam('id') zugegriffen werden. Sie können noch weitere Routen erstellen und an den Router anhängen, die dann ebenfalls dafür benutzt werden, um URLs auf Actions zu mappen. Zusätzliche Routen werden normalerweise eingerichtet, um einfacher verständliche oder kürzere URLs zu ermöglichen. Als Beispiel könnten wir eine Route erstellen, mit der der URL news/6 auf exakt die gleiche Weise wie news/index/view/id/6 gemappt wird. Dafür müssen wir ein Route-Objekt erstellen und es dem Router in der Bootstrap-Klasse einfügen, bevor die dispatch()Methode des Front-Controllers aufgerufen wird. Listing C.4 zeigt, wie das gemacht wird.
423
C Tipps und Tricks Listing C.4 Eine Route für news/{id-Nummer} in der Bootstrap-Klasse erstellen $defaults = array( Setzt Action 'module'=>'news', für die Route 'controller'=>'index', 'action'=>'view'); $newsRoute = new Zend_Controller_Router_Route( 'news/:id', $defaults);
Um eine neue Route einzurichten, müssen wir zuerst eine Zend_Controller_Router_ Route-Instanz erstellen und das Routen-Template festlegen (news/:id in diesem Fall o). Das Routen-Template verwendet den Doppelpunkt (:) als Präfix, um einen Platzhalternamen für eine Variable anzuzeigen. Variablenplatzhalter stehen dann innerhalb des Anfrageobjekts zur Verfügung und sind innerhalb des Controllers über die Elementmethode _getParam() wieder zugänglich. In diesem Fall wird die id als Variable gesetzt. Wenn das Routen-Template nicht genug Informationen enthält, um eine Route zu einer Action aufzulösen (d.h. um die Namen für Module, Controller und Actions anzugeben, die verwendet werden sollen), müssen sie im $defaults-Array spezifiziert werden n. Nachdem die Route definiert wurde, wird sie mit der addRoute()-Methode dem Router hinzugefügt p. Natürlich können so viele Routen wie notwendig hinzugefügt werden. Mit dem RoutenObjekt werden Variablenplatzhalter in der Route validiert, damit sie auch wirklich vom erforderlichen Typ sind. Außerdem sind dabei auch Standardeinstellungen erlaubt, falls der Platzhalter fehlt. Listing C.5 zeigt die Erstellung einer Route, die archivierte NewsEinträge für jedes Jahr auflistet, und zwar mit URLs in der Form news/archive/2008. Listing C.5 Eine Route mit Standards und Anforderungen erstellen $defaults = array( 'module'=>'news', 'controller'=>'index', Setzt Wert 'action'=>'view', 'year'=>2008); für :year $requirements = array('year'=>'\d{4}');
Erforderliche
Stellen nur für :year
$archiveRoute = new Zend_Controller_Router_Route('news/archive/:year', $defaults, $requirements);
Wieder setzen wir die Standardeinstellungen für Modul, Controller und Action, doch dieses Mal fügen wir auch einen Standard für den Platzhalter :year hinzu. Das bedeutet, wenn kein Wert angegeben ist, wird er auf 2008 gesetzt n. Die Anforderung für diese Route ist, dass der Platzhalter :year nur aus vier Stellen bestehen darf. Das wird über einen regulären Ausdruck im $requirements-Array gesetzt o. Wenn der :yearPlatzhalter die Anforderungen nicht erfüllt, passt die Route nicht und die nächste wird ausprobiert. Das bedeutet, dass ein URL wie news/archive/list bei dieser Route nicht zutreffen würde und stattdessen zu der List-Action des Archive-Controllers im News-Modul passt.
424
C.1 Tipps und Tricks für MVC Manche Routen benötigen die vollständige Flexibilität der Standard-Routenklasse nicht. Beispielsweise braucht eine Route wie /login, die der Login-Action des auth-Controllers zugeordnet ist, keine Variablen-Platzhalter. In diesen Situationen ist Zend_Controller_ Router_Route_Static schneller, weil kein Matching mit Variablen-Platzhaltern durchgeführt werden muss. Dessen Verwendung ist so wie die von Zend_Controller_Router_ Route (siehe Listing C.6), wo wir eine statische Route vom login/-URL zur login-Action des auth-Controllers im Standard-Modul erstellen. Listing C.6 Eine statische Route zu /login erstellen $defaults = array( 'module'=>'default', 'controller'=>'auth', 'action'=>'login');
Setzt Standard-
einstellungen für Route
Erstellt statische Route
$loginRoute = new Zend_Controller_Router_Route_Static('login', $defaults);
Die Erstellung einer statischen Route ist identisch mit der einer Standardroute, außer dass Sie nicht mit variablen Platzhaltern arbeiten können und deshalb Modul, Controller und Action innerhalb des Standard-Arrays definieren müssen n. Alternativ könnten Sie vielleicht auch einen URL haben wollen, der nicht über die Standardroute geroutet werden kann, weil er zu komplex ist. In dieser Situation kann man mit Zend_Controller_Router_Route_Regex arbeiten. Diese Route ist die leistungsfähigste und flexibelste, aber auch die komplexeste. Außerdem hat sie noch den zusätzlichen Vorteil, etwas schneller zu sein. Listing C.7 zeigt die neue Archiv-Route, wie sie anhand der regex-Route ausgedrückt wird. Listing C.7 News-Archiv-Route mit Zend_Controller_Router_Route_Regex $defaults = array( Definiert die Route anhand 'module'=>'news', eines regulären Ausdrucks 'controller'=>'index', 'action'=>'view'); $archiveRoute = new Zend_Controller_Router_Route_Regex( 'news/archive/(\d{4})', $defaults);
Weil die regex-Route keine Platzhaltervariablen verwendet, stehen die resultierenden Daten innerhalb des Anfrageobjekts zur Verfügung, und zwar über numerische Schlüssel, beginnend mit 1 für den ersten regulären Ausdruck in der Route. Im Fall der Route in Listing C.7 wird der Wert für das Jahr aus dem Controller wie folgt extrahiert: $year = $this->_getParam('1');
Das kann die Sache verkomplizieren, falls jemals die Route geändert wird, weil numerische Indizes sich auch ändern können. Änderungen an Routen sind relativ selten, und eine vernünftige Verwendung von Konstanten, die die Indizes enthalten sollen, würde den Änderungsaufwand abmildern.
425
C Tipps und Tricks Wir haben einige eher anspruchsvolle Einsatzmöglichkeiten des MVC-Systems abgedeckt und machen jetzt mit der Diagnose von Problemen in Ihrer Applikation weiter.
C.2 Diagnostik mit Zend_Log und Zend_Debug Zend Framework enthält die Komponenten Zend_Log und Zend_Debug, mit denen Sie Probleme in der Applikationslogik leichter aufspüren können. Zend_Debug wird für eher kurzfristige Datenprüfungen verwendet und Zend_Log fürs längerfristige Protokollieren von Informationen über die Applikation.
C.2.1 Zend_Debug Zend_Debug ist eine einfache Klasse mit nur einer Methode dump(), die Informationen über eine ihr übergebene Variable entweder ausgibt oder zurückgibt. Sie wird wie folgt verwendet: Zend_Debug::dump($var, 'title');
Der erste Parameter ist die Variable, die Sie darstellen wollen, und die zweite ist ein Label oder Titel für diese Daten. Intern verwendet die dump()-Methode var_dump() und fasst den Output in <pre>-Tags ein, wenn sie erkennt, dass der Output-Stream webbasiert ist. Sie wird die Daten auch escapen, falls PHP nicht im CLI-Modus verwendet wird. Mit Zend_Debug kann man sehr gut mal eben etwas auf die Schnelle testen. Wenn Sie in Ihre Applikation eine langfristige Diagnostik einbauen wollen, eignet sich Zend_Log besser.
C.2.2 Zend_Log ist so entworfen, dass damit Daten von mehreren Backends wie Dateien oder Datenbanktabellen geloggt werden können. Es ist recht einfach, mit Zend_Log Protokollinformationen in einer Datei zu speichern (siehe Listing C.8).
Zend_Log
Listing C.8 Daten mit Zend_Log protokollieren $writer = new Zend_Log_Writer_Stream( '/tmp/zf_log.txt'); $logger = new Zend_Log($writer);
Stream
zum Loggen
$logger->log('message to be stored', Zend_Log::INFO);
Erstellt Logger
Loggt eine
Info-Nachricht
426
C.2 Diagnostik mit Zend_Log und Zend_Debug Das Zend_Log-Objekt braucht ein Writer-Objekt, um die Daten zu speichern. In diesem Fall erstellen wir einen Stream-Writer in einer Datei namens /tmp/zf_log.txt n und hängen sie an das Zend_Log-Objekt an o. Zum Speichern einer Nachricht wird die Elementfunktion log() verwendet p. Wenn Sie eine Nachricht mit log() speichern, müssen Sie ihre Priorität angeben. Die verfügbaren Prioritäten werden in Tabelle C.3 aufgelistet. Tabelle C.3 Prioritäten für log()-Nachrichten bei Zend_Log Name
Hinweis (Notice): Normale, aber signifikante Bedingungen
Zend_Log::INFO
6
Information (Informational): Infonachricht
Zend_Log::DEBUG
7
Fehlerbehebung (Debug): Debug-Nachrichten
Die Prioritäten stammen aus dem BSD-Syslog-Protokoll und werden in der Reihenfolge der Bedeutung aufgelistet, wobei EMERGE-Nachrichten die wichtigsten sind. Bei jeder Priorität gibt es eine Shortcut-Methode, die nach dem Prioritätsnamen benannt ist, der als Alias für log() fungiert und bei dem die korrekte Priorität gesetzt ist. Das bedeutet, dass diese beiden Befehle exakt das Gleiche loggen: $logger->log('Kritisches Problem', Zend_Log::CRIT); $logger->crit('Kritisches Problem');
Diese Convenience-Methoden dienen vor allem als Kurzformen, damit Sie weniger Code zu tippen haben. Bei einer typischen Applikation erstellen Sie beim Start der Applikation das $loggerObjekt im Bootstrap und speichern es in der Registry, um es im Rest der Applikation zu verwenden: $writer = new Zend_Log_Writer_Stream(ROOT_DIR.'/tmp/log.txt'); $logger = new Zend_Log($writer); Zend_Registry::set('logger', $logger);
Wenn Sie das Zend_Log-Objekt erstellen, können Sie sich den Writer aussuchen. Es stehen vier Writer zur Verfügung (siehe Listing C.4).
427
C Tipps und Tricks Tabelle C.4 Zend_Log_Writer-Objekte Name
Einsatzgebiet
Zend_Log_Writer_Stream
Speichert Logs in Dateien oder andere Streams. Mit dem ‚php://output’-Stream kann man Logs im Output-Puffer darstellen.
Zend_Log_Writer_Db
Speichert Logs in Datenbankeinträge. Sie müssen den Level und die Nachricht zwei Feldern in einer Tabelle zuordnen.
Zend_Log_Writer_Firebug
Sendet Log-Nachrichten an die Konsole der Firefox-Extension Firebug.
Zend_Log_Writer_Null
Verwirft alle Log-Nachrichten. Das ist praktisch, wenn man das Logging beim Testen ausschalten oder generell deaktivieren will.
Um den Level des durchgeführten Loggings zu steuern, nimmt man ein Zend_Log_Filter_ Priority-Objekt. Damit wird die minimale Priorität der zu loggenden Nachricht festgelegt, und alle Nachrichten mit geringerer Priorität werden nicht protokolliert. Der Filter wird an das Log-Objekt über addFilter() angehängt, wenn und sobald das erforderlich ist. Normalerweise wird das beim Zeitpunkt der Erstellung eingefügt, und die gewählte Priorität ist normalerweise bei einer Live-Site höher als bei einer Test-Site. Um das Logging auf Nachrichten zu beschränken, die mindestens CRIT sind, wird dieser Code verwendet: $filter = new Zend_Log_Filter_Priority(Zend_Log::CRIT); $logger->addFilter($filter);
Das bedeutet, alle Informationsnachrichten werden verworfen und nur die besonders wichtigen geloggt. Bei einer Live-Site wird gewährleistet, dass die Performance der Applikation nicht durch die fürs Logging nötige Zeit behindert wird. Wir wenden unsere Aufmerksamkeit nun der Profiler-Komponente in Zend_Db zu und schauen, wie wir die SQL-Anweisungen darstellen können, die gestartet werden.
C.3 Zend_Db_Profiler hängt sich an einen Zend_Db-Adapter an. Damit können wir das SQL von Abfragen sehen, die gestartet werden, und wie lange jede gebraucht hat. Wir können anhand dieser Informationen unsere Optimierungsbemühungen pointiert setzen, entweder durch Caching der Resultate lang laufender Abfragen oder durch Optimierung der Abfrage selbst, indem man z.B. Tabellenindizes feinjustiert.
Zend_Db_Profiler
Um den Datenbankadapter über ein config-Objekt zu konfigurieren, schalten Sie den Profiler am besten dadurch ein, dass er in der INI- oder XML-Datei gesetzt wird. Wir nehmen diesen Mechanismus für Places, und Listing C.9 zeigt die config.ini von Places mit aktiviertem Profiling.
Alle Daten im params-Abschnitt werden an den Zend_Db-Datenbankadapter übergeben, der dann ein Zend_Db_Profiler-Objekt erstellt und aktiviert. Um die Profilinformation auszulesen, kann man die getLastQueryProfile()-Methode des Profilers nehmen. Listing C.10 zeigt, wie man die Abfragedaten aus der fetchLatest()Methode des Places-Models in application/models/Places.php in der Places-Applikation loggt. Listing C.10 SQL-Abfragedaten in der fetchLatest()-Modelfunktion protokollieren public function fetchLatest($count = 10) { $result = $this->fetchAll(null, 'date_created DESC', $count);
Zuerst starten wir die fetchAll()-Abfrage n und speichern die zurückzugebenden Resultate am Ende der Methode. Wir lesen die Profildaten für diese Abfrage aus, indem wir eine Instanz des Profilers o holen und dann getLastQueryProfile() aufrufen p. Das Abfrageprofil hat ein paar nützliche Methoden, mit denen wir den zu loggenden String erstellen q. Wie in Abschnitt C.2.2 erläutert, können wir die Instanz des -Objekts (logger in diesem Fall) aus der Zend_Registry auslesen und die Nachricht entsprechend der DebugPriorität loggen r. Der resultierende Protokolleintrag sieht wie folgt aus: 2008-02-02T17:00:00+00:00 DEBUG (7): Query: "SELECT `places`.* FROM `places` ORDER BY `date_created` DESC LIMIT 10", Params: , Time: 0.70691108703613ms
429
C Tipps und Tricks In diesem Fall gab es keine gebundenen Parameter, und der Params-Abschnitt ist somit leer. Das liegt daran, dass die fetchAll()-Abfrage einfach die Resultate nach Zeitpunkt der Erstellung sortiert und sie auf das erste Mal beschränkt. Der Profiler loggt alle Events, während er eingeschaltet ist. Also können die Daten für alle Abfragen direkt am Ende der Verarbeitung extrahiert und bei Bedarf protokolliert werden. In diesem Fall müssen Sie keine vorhandenen Model-Methoden ändern und könnten einfach die Profildaten nach dem Aufruf von dispatch() im Bootstrap loggen. Listing C.11 zeigt ein Beispiel, wobei davon ausgegangen wird, dass die Zend_Log- und Zend_DbObjekte in der Registry gespeichert worden sind. Listing C.11 Alle SQL-Profildaten am Ende des Dispatchings loggen $frontController->dispatch(); $logger = Zend_Registry::get('logger'); $db = Zend_Registry::get('db');
Startet Liest Db und Applikation Log aus Registry aus
Wenn dispatch() fertig ist n, holen wir die db- und logger-Objekte aus der Registry o und dann den profiler aus dem Datenbankobjekt. Das profiler-Objekt hat einige Methoden für allgemeine Metriken, wovon wir getTotalElapsedSecs() und getTotalNumQueries() nehmen, um einen Eindruck davon zu bekommen, wie viele Datenbankaufrufe gemacht wurden und wie lange all die Datenbankabfragen gedauert haben p. Die getQueryProfiles()-Methode gibt ein Array mit Zend_Db_Profiler_Query-Objekten zurück, und wir iterieren durch sie anhand der verschiedenen Elementfunktionen, um einen einzelnen Textstring mit Informationen über jede Abfrage innerhalb des Arrays zu erstellen q. Wir formatieren einen einzelnen String, der alle Informationen enthält, die wir loggen wollen r, und speichern unter der Debug-Priorität ins Log s.
430
C.4 Zusammenfassung Hier passiert ganz schön viel. Also wäre es schlau, das in eine eigene Funktion zu fakturieren. Das produzierte Log sieht wie folgt aus: 2008-04-06T20:04:58+01:00 DEBUG (7): 3 queries in 3.029 milliseconds Queries: 0 - Query: "connect", Params: , Time: 0.603 ms 1 - Query: "DESCRIBE `places`", Params: , Time: 1.895 ms 2 - Query: "SELECT `places`.* FROM `places` ORDER BY `date_created` DESC LIMIT 10", Params: , Time: 0.531 ms
Diese Information verrät uns alles, was wir über jede Abfrage wissen müssen, die bei der Generierung der Seite stattgefunden hat. In diesem Fall sehen wir, dass es am längsten gedauert hat, die Details der places-Tabelle mittels DESCRIBE zu bekommen. Also können wir uns dafür entscheiden, die Details des Datenbankschemas zu cachen, und zwar anhand der setDefaultMetadataCache()-Methode von Zend_Db_Table_Abstract.
C.4 Zusammenfassung In diesem Anhang beschäftigten wir uns mit den seltener verwendeten Features im Zend Framework. Das MVC-System ist sehr flexibel, und insbesondere das Modulsystem erlaubt falls nötig die weitere Separation der Codebasis. Mit Routing bekommen Ihre User URLs, die lesefreundlich sind und auch gut von Suchmaschinen verarbeitet werden können. Die drei angegebenen Routen bieten viele Optionen, doch wenn sie Ihren Ansprüchen nicht genügen, ist das System flexibel genug, damit Sie Ihr eigenes Router-Objekt einbauen oder eigene Routen definieren können, die an den rewrite-Router angehängt werden. Zwar glauben wir alle fest dran, dass unser Code keine Bugs hat, aber es ist schon praktisch, wenn man auf einfache Weise eine Variable inspizieren oder den Programmfluss in einer Datei loggen kann. Mit Zend_Debug und Zend_Log überwachen Sie das Geschehen in Ihrer Applikation, falls etwas schief geht und Sie die Ursache finden müssen. Für Datenbankaufrufe mit Zend_Db stellt der eingebaute Profiler Timing-Informationen bereit und informiert darüber, welche Abfrage gerade ausgeführt wird. Bei der Integration von Zend_Log bekommen Sie einen leistungsfähigen Mechanismus zum Aufdecken von Datenbank-Bottlenecks an die Hand. Somit können Sie sich auf die Optimierung konzentrieren und dort ansetzen, wo Sie die größten Auswirkungen erzielen.
Base 26, 299 Blogger 26, 299 Calendar 110 CodeSearch 299 Code-Suche 26 Documents List 299 Kalendar 299 Kalender 26 Notebook 299 Provisioning 299 Suggest 109 Text und Tabellen 299 YouTube 26 Google Data 298
API 299, 315 Grad 380 Grafikstatus 379
speichern 381 Grauskala 373 Gravatar 302 Gruber, John 238
Übersicht 24 Internet Engineering Task Force (IETF) 226 Internet Explorer 110 Internet Media File System (IMFS) 302 isAllowed() 174 isDispatched() 106 ISO8601
Format 287 ist ein-Beziehung 403 isValid() 180, 220 isXhtml() 103 Iterator 409
W W3C siehe World Wide Web Consortium 271 WAI siehe Web Accessibility Initiative 63 Web Accessibility Initiative (WAI) 63, 110 Web Services Description Language (WSDL) 271 Webanfrage 33 Webserver 251 Webservice 272
Amazon-Beispiel 305 Arbeitsweise 274 Caching 308
Website
Aufgabe der Site Places 60 Benutzerschnittstelle 62 Planung 60 Spezifikation 60 Story 60 übersetzen 346 Werbeanzeige 305 Wert
komma- oder tabulatorgetrennter 272 while() 393 Wiederverwendbarkeit 14 Windows 347
Ecto 283 Probleme mit setlocale() 360 Winer, Dave 273 WordPress 304 wordwrap() 372 World Wide Web Consortium (W3C) 271, 273 Worttrennungszeichen 421 wrapText() 371 write_control, Option 329 WSDL siehe Web Services Description Language 271
X X_REQUESTED_WITH 121 XLIFF 349 XML 111, 272, 277
Beziehung zwischen Rolle, Ressource und Privileg 167 controller-zentrierte Regelmethoden 172 Einrichten von Zugriffsregeln 174 isAllowed() 174 Privileg 167, 169 Ressource 156, 167, 168 Rolle 167, 168 Zend_Acl 168 Zend_Acl_Role 168 Zugriffskontrollliste
rollenbasierte 167 Zugriffssteuerung
Übersicht 24
Struts 2 im Einsatz
Brown/Davis/Stanlick Struts 2 im Einsatz 478 Seiten ISBN 978-3-446-41575-1
Das klassische Struts ist nach wie vor das am meisten genutzte Entwicklungs-Framework für Java-Webanwendungen. Dieses Buch bietet Ihnen präzises und bewährtes Praxiswissen, egal ob Sie schon Erfahrung mit Struts 1 haben oder sich zum ersten Mal mit Struts auseinandersetzen. Vorausgesetzt werden nur grundlegende Kenntnisse der Webentwicklung mit Java. Das Autorenteam unter der Leitung von Don Brown, einem der führenden Entwickler von Struts 2, zeigt Ihnen, wie Sie mit Struts 2 professionelle Web-Applikationen entwickeln. Sie lernen Komponenten wie Actions, Interceptors, Results und die auf Annotationen basierende Konfiguration zu beherrschen. Struts-1-Entwickler werden von den ausführlichen Kapiteln über Plug-Ins, FreeMarker Templates und über die Migration von Struts 1 und WebWork profitieren. Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer
JBoss im Detail
Jamae/Johnson JBoss im Einsatz Den JBoss Application Server konfigurieren 543 Seiten ISBN 978-3-446-41574-4
Dieses Buch erläutert den JBoss 5 Application Server vollständig, von der Installation und Konfiguration bis zum Deployment von Anwendungen. Es konzentriert sich auf die Dinge, die JBoss von anderen Java EE Servern unterscheiden. Die Autoren führen Sie durch die Konfiguration der Komponenten-Container wie den JBoss Web Server, den EJB3 Server und JBoss Messaging und vermitteln Ihnen detailliertes Know-how zu Services wie Sicherheit, Performance und Clustering. Die Autoren, beide erfahrene Experten in der Entwicklung und Administration von JBoss, bieten hilfreiche Hintergrundinformationen zu vielen Themen und reichern sie um Tipps und Erfahrungen aus ihrer Praxis an.
Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer
Frischer Wind für Java.
Walls Spring im Einsatz 676 Seiten. ISBN 978-3-446-41240-8
Spring ist ein frischer Wind in der Java-Landschaft. Dieses Framework für Java EE verbindet die Macht von Enterprise Applikationen mit der Einfachheit von einfachen Java-Objekten (Plain Old Java Objects, POJOs) - und macht so dem Java-Entwickler das Leben leicht. Diese zweite Auflage des Bestsellers Spring in Action deckt die Version 2.0 und alle ihre neuen Features ab. Das Buch beginnt mit den grundlegenden Konzepten von Spring und führt den Leser rasch dazu, dieses Framework aktiv kennen zu lernen. Kleine Code-Beispiele und eine schrittweise ausgebaute eigene Anwendung zeigen, wie einfache und effiziente JEE-Applikationen mit Spring entwickelt werden. Der Leser erfährt, wie Persistenz-Probleme gelöst werden, wie mit asynchronen Nachrichten umgegangen wird und wie man Remote Services erstellt und nutzt. Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer
Mehr PHP gibt’s nicht!
Krause PHP 5 – Grundlagen und Profiwissen 1344 Seiten. Mit CD. ISBN 3-446-40334-5
In diesem Standardwerk finden Sie Informationen zu allen Neuerungen in PHP 5. Hierzu zählen unter anderem die OOP- und XML-Erweiterungen (Interfaces, Exceptions, DOMXML, SimpleXML, XSLT), die WebdienstProgrammierung (SOAPExtension), die integrierte Datenbank SQLite und das Sprachanalysewerkzeug Reflection. Das Buch eignet sich für Einsteiger als solide Einführung, bietet Fortgeschrittenen eine dauerhafte Arbeitsgrundlage und liefert Profis ein ausführliches Nachschlagewerk. Die Kurzreferenz mit der alphabetischen Funktions-Übersicht unterstützt den Anwender bei der täglichen Arbeit. Die PDF-Fassung des Buches erleichtert den Zugang zu den Informationen.
Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer
ZEND FRAMEWORK IM EINSATZ // Zend Framework ist das wohl meistverbreitete PHP-basierte Framework. Mit ihm lassen sich vergleichsweise einfach und rasch leistungsfähige und stabile PHP-basierte Applikationen erstellen. Das vorliegende Buch ist ein kompaktes Tutorial, das den Leser in wesentliche Aspekte der Arbeit mit dem Zend Framework einführt. Die Autoren, selbst aktive Mitglieder der Zend Framework Community, stellen dazu nach einem allgemeinen Überblick zentrale Konzepte des Zend Frameworks vor, verwenden wichtige Techniken und Funktionen der PHP-Programmierung wie Data Handling, Forms oder Authentication, binden AJAX-basierte Features ein und beschäftigen sich mit Aspekten der Sicherheit, Performance und von (Unit-)Tests. Durch die komplexe Beispielapplikation, deren Entwicklung dem Leser den praktischen Einsatz des Frameworks vor Augen führt, die vielen Beispiele und die zahlreichen Tipps eignet sich das Buch als sinnvolle Ergänzung der Dokumentation des Zend Frameworks.
STIMMEN ZUR US-AUSGABE // »Compelling … A great introduction to the Zend Framework.« Thomas Weidner, Team Leader, Zend Framework // »Thorough, detailed. You couldn’t ask for a better guide.« Matthew Weier O’Phinney, Software Architect, Zend Framework // »A must-have resource. Picks up where the documentation leaves off.« David Hanson, D.A. Hanson Consulting LLC
rob ALLEN ist Webentwickler und hat für die Zend Framework Community unter anderem die Zend_Config-Komponente entwickelt. nick LO ist Webdesigner und Webentwickler und hat mehrere Online-Tutorials zum Zend Framework verfasst. steven BROWN ist erfahrener PHP-, Java-, ActionScript- und JavaScript-Entwickler und inzwischen glühender Anhänger des Zend Frameworks. www.hanser.de/computer ISBN 978-3-446-41576-8
(Web-)Programmierer und Entwickler mit Vorkenntnissen in PHP
9
783446 415768
Systemvoraussetzungen für eBook-inside: Internet-Verbindung, Adobe Acrobat Reader Version 6 oder 7 (kompatibel mit Windows ab Windows 2000 oder Mac OS X). Ab Adobe Reader 8 muss zusätzlich der eBookreader Adobe Digital Editions installiert sein.
HOLEN SIE MEHR RAUS AUS PHP // ■ Beschreibt die wesentlichen Komponenten des Zend Frameworks ■ Orientiert sich an wichtigen Techniken und Funktionen der PHP-Programmierung ■ Enthält zahlreiche Beispiele aus der Entwicklungspraxis der Autoren ■ Mit einer durchgehenden Beispielapplikation ■ Die Codebeispiele verfügbar unter www.downloads.hanser.de