Zend Framework im Einsatz

FRAMEWORK IM EINSATZ ZEND FRAMEWORK IM EINSATZ rob ALLEN nick LO steven BROWN Allen, Lo, Brown Zend Framework im Ein...
Author:  Rob Allen |  Nick Lo |  Steven Brown

36 downloads 1735 Views 6MB Size Report

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!

Report copyright / DMCA form



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';

 Vertraut dem

User-Input nicht

$filters = array('q' => array('StringTrim' , 'StripTags')); 'required')); $input = new Zend_Filter_Input($filters, $validators, $_GET); if ($input->isValid()) { $this->view->messages = ''; Prüft, ob Validiert $q = $input->getEscaped('q'); Suchanfrage existiert $this->view->q = $q;



// do search $index = Places_Search_Lucene::open( SearchIndexer::getIndexDirectory()); $results = $index->find($q); $this->view->results = $results;



 Öffnet Suchindex  Findet Ergebnisse  Weist der View

} else { $this->view->messages = $input->getMessages(); $validators = array('q' => array('presence' => }

Fehlermeldungen zu

}

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



Umgang mit Sonderzeichen

    results as $result) : ?>
  • getSearchResultUrl( $result->class, $result->id); ?>"> escape($result->title);?>
    escape($result->summary);?>


  • Liest URL aus Stellt Gewichtung als Kommentar dar

    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

    Zend_Mail::send()

    ---- ------- --- ----- --- - --- - ----- -Router Router

    Das Internet

    Router

    Router

    Warten auf den Versand

    Warten auf den Empfang

    Versendender Mailserver (MTA)

    Empfangender Mailserver (MTA)

    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); }

    Übergibt SupportTicket-Daten plus optionale ID

    public function saveIssue(array $data, $id = null) { $filterStripTags = new Zend_Filter_StripTags; $filterFormat = new Zend_Filter; $filterFormat->addFilter(new Zend_Filter_StripTags) ->addFilter(new ThirdParty_Filter_Markdown); if (null === $id) { $row = $this->_supportTable->createRow(); $row->date_created = date('Y-m-d H:i:s'); } else { $row = $this->getIssue($id); } $row->user_id = Zend_Auth::getInstance()-> getIdentity()->id; $row->type = $filterStripTags-> filter($data['type']); $row->priority = (int) $data['priority']; $row->status = $filterStripTags-> filter($data['status']); $row->title = $filterStripTags-> filter($data['title']); $row->body = $filterStripTags-> filter($data['body']); $row->body_formatted = $filterFormat-> filter($data['body']); $id = $row->save(); return $id; }

    Richtet Datenfilter ein

    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,

    falls erforderlich if ($html) { $mail->setBodyHtml( $supportIssue->current()->body_formatted ); } $mail->setBodyText($supportIssue->current()->body); $mail->setFrom('[email protected]', 'The System'); $mail->addTo('[email protected]', 'Support Team'); $mail->addHeader( 'X-Priority', $supportIssue->current()->priority, true ); $mail->send(); } }

    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

    Holt allgemeine Informationen über die Datei

    an die E-Mail $at = $mail->addAttachment($fileBody); $at->filename = $fileName; Setzt optionale $at->type = $fileType; Einstellungen $at->disposition = Zend_Mime::DISPOSITION_INLINE; fürs Attachment $at->encoding = Zend_Mime::ENCODING_BASE64;

    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.

    245

    10 E-Mails Listing 10.14 Integration einer Mail-lesen-Funktion in der Klasse Support public function readMail(Zend_Mail_Storage_Abstract $mail) { foreach ($mail as $messageNum => $message) { $part = $message; while ($part->isMultipart()) { $part = $message->getPart(1); } if (strtok($part->contentType, ';') == 'text/plain') { $plainTextContent = $part->getContent(); } $data['priority'] = isset( $message->xPriority) ? $message->xPriority : 3; $data['status'] = 'open'; $data['title'] = $message->subject; $data['body'] = $plainTextContent; $data['reported_by'] = $message->from; $id = $this->saveIssue($data); $messageString = ''; foreach ($message->getHeaders() as $name => $value) { $messageString .= $name . ': ' . $value . "\n"; } $messageString .= "\n" . $message->getContent(); file_put_contents(getcwd() . '/support/' . $id . '/email.txt', $messageString ); } }

    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

    [production : general] database.username = production database.password = production123 database.dbname = production_main

    Spezifiziert Einstellungen für finalen Production-Server

    [rob-dev : general] database.username = rob database.password = roballen123 database.dbname = robs_dev

    Spezifiziert Einstellungen für Robs Development-Server

    [nick-dev : general] database.username = nick database.password = nicklo123 database.dbname = nicks_dev

    Spezifiziert Einstellungen für nick-dev

    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

    Setzt Einstellungen für Staging-Host

    DocumentRoot /path/to/Sites/places-stage/web_root ServerName places-stage

    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: $ $ $ $

    cd /Pfad/zu/svn/places/ svn mkdir trunk svn mkdir tags svn mkdir branches

    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();

     Falls nicht anders angegeben, ist RSS Standard

     Holt Feed-Daten aus Datenbank

    $channel = array( 'title' => 'Places', Erstellt -Element 'link' => 'http://places/', 'description' => 'All the latest articles', 'charset' => 'UTF-8', 'entries' => array() );

    

    276

    12.2 Die Produktion und Verarbeitung von Feeds mit Zend_Feed

     Erstellt

    -Elemente Generiert foreach ($rowset as $item) { $channel['entries'][] = array( Output 'title' => $item->title, 'link' => 'http://places/article/index/id/' . $item->id . '/', Importiert Array 'description' => $item->body in Zend_Feed ); } $feed = Zend_Feed::importArray($channel, $format); $feed->send(); $this->_helper->viewRenderer->setNoRender(); Deaktiviert Rendern $this->_helper->layout()->disableLayout();

    

    

    }

     von View und Layout

    }

    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:

    feed->link(); ?>"> feed->title(); ?>

    feed->description(); ?>
    feed as $item): ?>

    link; ?>"> title(); ?>

    description(); ?>


    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

    

    

    

    passen müssen

    ) {

    

    $identity = $this->_auth->authenticate( Prüft Autorisierung $username, $password); if(false === $identity) { throw new Exception('Authentication Failed'); }

     Erstellt Array,

    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

    286

    Nativer PHP-Typ

    XML-RPC-Typ

    Zend_XmlRpc_Value-Objekt

    Boolean



    Zend_XmlRpc_Value_Boolean

    Integer

    oder

    Zend_XmlRpc_Value_Integer

    Double

    <double>

    Zend_XmlRpc_Value_Double

    String

    <string> (der Standardtyp) Zend_XmlRpc_Value_String

    dateTime.iso8601



    Zend_XmlRpc_Value_DateTime

    Base64



    Zend_XmlRpc_Value_Base64

    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';

    Setzt Dateipfad für gecachete Information

    Prüft Cache vor Anhängen der Klassen

    $server->setClass('Blogger', 'blogger'); $server->setClass('Metaweblog', 'metaWeblog'); $server->setClass('MovableType', 'mt'); Zend_XmlRpc_Server_Cache::save( $cacheFile, $server);

    Speichert introspektive Information im Cache

    }

    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);

    Client-Objekt mit Server-URL

    $filterStripTags = new Zend_Filter_StripTags; $id = (int) $_POST['id']; $publish = (bool) $_POST['publish']; $structData = array(

    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);

    Formatiert Daten in

    einen HTTP-POST try { $response = $client->request( Stellt Zend_Http_Client::POST Anfrage ); } catch (Zend_Http_Client_Exception $e) { echo $e->getCode() . ': ' . $e->getMessage() . "\n"; }

    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, }

    falls keine Ergebnisse da sind

    zugebendes

    HTML ein $xhtml = ''; return $xhtml;

    Gibt HTML 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

    Setzt Cache-

    Verzeichnis $backendOptions = array( Holt Zend_Cache_ 'cache_dir' => $this->cacheDir Core-Objekt mit ); Optionen $this->cache = Zend_Cache::factory( 'Core','File', $frontendOptions,$backendOptions); } public function amazonAds($keywords, $amount=3, $elementId='amazonads') { if(!$xhtml = $this->cache->load('amazon')) { $amazon = new Amazon; $results = $amazon->search($keywords); if(null === $results) { return null; }

    Versucht, gecachete Version zu laden Fährt mit Abfrage fort, falls keine gecachete Version

    $xhtml = ''; $this->cache->save($xhtml, 'amazon'); } return $xhtml; }

    Speichert Output als gecachete Version

    }

    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): ?>

    Images from Flickr



    vorhanden sind

    Macht Schleife durch Ergebnisse

    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
    /"> Zeigt verlinkten title->text; ?> Titel und Beschreibung
    description->text; ?>


    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); ?>
    /"> Zeigt verlinkten mediaGroup->title->text; ?> Titel und Beschreibung
    mediaGroup->description->text; ?>


    319

    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; <param name="movie" value="http://www.youtube.com/v/ videoId; ?>&rel=0"> <param name="wmode" value="transparent"> <embed src="http://www.youtube.com/v/ videoId; ?>&rel=0" type="application/x-shockwave-flash" wmode="transparent" width="425" height="355">

    ?>



    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 ( //...

    332

    14.3 Die Implementierung von Zend_Cache

    ); $queryCache = Zend_Cache::factory( 'Core', 'File', $frontendOptions, $backendOptions ); $cacheName = 'product_id_' . $id; if(!($result = $queryCache->load($cacheName))) { $result = $this->fetchRow(array('id = ?' => $id)); $queryCache->save($result, $cacheName); } return $result; } }

    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:

    339

    14 Das Caching beschleunigen 'regexps' => array( '^/admin/' => array( 'cache' => false, ), '^/admin/products/' => array( 'cache' => true; ), ),

    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';

    Erstellt ein ÜbersetzungsArray

    $translate = new Zend_Translate('array', $data, 'en'); print $translate->_("hello")."\n"; print "=======\n"; printf($translate->_('today %1$s')."\n", date('d/m/Y'));

    Erstellt eine Instanz von Zend_ Translate

    Gibt die Übersetzung

     von „hello“ aus

     Arbeitet mit den printf()Platzhaltern

    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');

    Verwendet den gettextAdapter

    print $translate->_("hello")."\n"; print "=======\n"; printf($translate->_('today %1$s')."\n", date('d/m/Y'));

    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);

     Erstellt Zend_

    Zend_Registry::set('lang', $lang); Zend_Registry::set('localeString', $localeString); Zend_Registry::set('locale', $locale); Zend_Registry::set('Zend_Translate', $translate); }

    356

    Translate-Objekt

     Weist der

    Registry zu

    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

    Setzt Sprachparameter

    url(array( 'lang'=>$this->localeString, 'module'=>'default' Setzt andere 'controller'=>'review', Parameter fürs Routing 'action'=>'add', 'placeId'=>$this->place->id ), null, true);?>"> Add review

    

    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

    

    $locale = Zend_Registry::get('locale'); $date = new Zend_Date($timestamp, null, $locale); return $date->get($format); } }

     Erstellt Zend_Date-Objekt Holt Datum als

     formatierten String

    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

    $_boldFont; $_year; $_headTitle; $_introText; $_graphData;

     Instanziiert

    Zend_Pdf_Page als Eigenschaft

     Setzt Startpunkt

    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: ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ ƒ

    Zend_Pdf_Font::FONT_COURIER Zend_Pdf_Font::FONT_COURIER_BOLD Zend_Pdf_Font::FONT_COURIER_OBLIQUE Zend_Pdf_Font::FONT_COURIER_BOLD_OBLIQUE Zend_Pdf_Font::FONT_HELVETICA Zend_Pdf_Font::FONT_HELVETICA_BOLD Zend_Pdf_Font::FONT_HELVETICA_OBLIQUE Zend_Pdf_Font::FONT_HELVETICA_BOLD_OBLIQUE Zend_Pdf_Font::FONT_SYMBOL Zend_Pdf_Font::FONT_TIMES_ROMAN Zend_Pdf_Font::FONT_TIMES_BOLD Zend_Pdf_Font::FONT_TIMES_ITALIC Zend_Pdf_Font::FONT_TIMES_BOLD_ITALIC Zend_Pdf_Font::FONT_ZAPFDINGBATS

    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

    

    protected function getGraphSection() Speichert Grafikstatus { $this->_page->saveGS(); $this->_page->setFont($this->_boldFont, 16); $this->_yPosition -= 20; $this->_page->drawText('Monthly statistics for ' . $this->_year, Zeichnet $this->_leftMargin, Diagramm$this->_yPosition); überschrift $this->_yPosition -= 10; $this->_page->drawLine($this->_leftMargin, $this->_yPosition, $this->_pageWidth - $this->_leftMargin, $this->_yPosition); $this->_yPosition -= 40;

    

    

    $graphY = $this->_yPosition Setzt anfängliche- und - max($this->_graphData); y-Achsen für Diagramm $graphX = $this->_leftMargin; $columnWidth = 40; Setzt Spaltenbreite $date = new Zend_Date();

     der Grafik

     Schleife über

    Diagrammdaten foreach ($this->_graphData as $key => $value ) { $graphFill = $key % 2 == 1 ? Setzt abwechselnde '#FA9300' : '#990033'; $this->_page->setFillColor( Spaltenfarben new Zend_Pdf_Color_Html( $graphFill)); Zeichnet $this->_page->drawText($value, Spaltenwert $graphX + ($columnWidth/3), $graphY + $value);

    

    

    $this->_page->drawRectangle($graphX, $graphY, $graphX + $columnWidth, $graphY + $value, Zend_Pdf_Page::SHAPE_DRAW_FILL ); $yPosition = $graphY - 20; $date->set($key + 1, Zend_Date::MONTH_SHORT); $this->_page->drawText($date->get( Zend_Date::MONTH_NAME_SHORT), $graphX + ($columnWidth/8), $yPosition); $graphX += $columnWidth; } $this->_yPosition = $yPosition - 20; $this->_page->restoreGS(); }

    378

    Stellt Grafikzustand wieder her

    Zeichnet

    Rechteck für Spalte

    Setzt y-Position 20 Punkt unter Diagramm

    Setzt und

    zeichnet abgekürzten Monatsnamen

    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º



    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

    

    

    $page1->setGraphData( array(30,25,60,90,10,45,80,30,80,20,0,0) ); $report->addPage($page1);

     Fügt Seite in

    Dokument ein

    header( 'Content-Type: application/pdf; charset=UTF-8' ); echo $report->getDocument()->render(); $this->_helper->viewRenderer->setNoRender(); $this->_helper->layout->disableLayout(); }

    Setzt korrekte

     HTTP-Header

    Deaktiviert

    Rendern von View und Layout

    Gibt gerendertes Dokument aus

    

    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.

    Monthly statistics for 2008 90

    80

    80

    60 45 30

    30

    25 10

    Jan

    Feb Mar Apr May Jun

    Jul

    20

    Aug Sep Oct

    0 0 Nov Dec

    Meeting Notes

    © 2008 Places to take the kids

    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(); }

     Speichert

    aktuellen include-Pfad

    public function preDispatch(Zend_Controller_Request_Abstract $request) { $moduleName = $request->getModuleName(); $fc = Zend_Controller_Front::getInstance(); $controllerDir = $fc->getControllerDirectory( $moduleName); $moduleDir = dirname($controllerDir); $modelsDir = $moduleDir.'/models'; set_include_path($moduleDir . PATH_SEPARATOR . $this->_baseIncludePath); } }

    420

     Setzt modelsVerzeichnis

     Setzt

    include-Pfad

    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);

    

    $router = $frontController->getRouter(); $router->addRoute('news', $newsRoute);

     Richtet neue Route ein

     Fügt Route ein

    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

    Wert

    Verwendung

    Zend_Log::EMERGE

    0

    Notfall (Emergency): System ist nicht verwendbar

    Zend_Log::ALERT

    1

    Benachrichtigung (Alert): Sofortige Aktion erforderlich

    Zend_Log::CRIT

    2

    Kritisch (Critical): kritische Bedingungen

    Zend_Log::ERR

    3

    Fehler (Error): Fehlerbedingungen

    Zend_Log::WARN

    4

    Warnung (Warning): Warnbedingungen

    Zend_Log::NOTICE

    5

    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.

    428

    C.3 Zend_Db_Profiler Listing C.9 Datenbank-Profiling in config.ini aktivieren [general] db.adapter = PDO_MYSQL db.params.host = localhost db.params.username = zfia db.params.password = zfia db.params.dbname = places db.params.profiler = true

    Aktiviert Profiler

    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);

     Startet

    Abfrage

     Liest ProfilerInstanz aus

    $profiler = $this->_db->getProfiler(); $p = $profiler->getLastQueryProfile(); $msg = 'Query: "' . $p->getQuery() . '"'; $msg .= ', Params: ' . implode(',', $p->getQueryParams()); $msg .= ', Time: ' . $p->getElapsedSecs() * 1000 . 'ms'; $logger = Zend_Registry::get('logger'); $logger->debug($msg); return $result;

     Formatiert

     Speichert in Logger

    Profildaten

    Sammelt Profil der letzten Abfrage

    

    }

    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

    $profiler = $db->getProfiler(); $totalTime = $profiler->getTotalElapsedSecs() * 1000; $queryCount = $profiler->getTotalNumQueries(); foreach ($profiler->getQueryProfiles() as $i=>$query) { $ms = $query->getElapsedSecs() * 1000;

     Sammelt

    Gesamtzahl

     Formatiert

    Log-Nachricht für jede Abfrage

    $msg = $i . ' - Query: "' . $query->getQuery() $msg .= ', Params: ' . implode(',', $query->getQueryParams()); $msg .= ', Time: ' . number_format($ms, 3) . ' ms'; $messages[] = $msg; } $log = $queryCount . ' queries in ' . number_format($totalTime, 3) . ' milliseconds' . "\n"; $log .= "Queries:\n"; $log .= implode("\n", $messages); $logger->debug($log);

     Formatiert

    gesamte Log-Nachricht

     Schreibt in Log

    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.

    431

    Register $ $, Symbol 387 $_dependantTables 149 $_name 138 $_referenceMap 149, 152 $_rowClass 143 $_SERVER['REQUEST_URI'] 337 $translationStrings 357

    . ., Punkt 68 .htaccess 38 .htaccess, Datei 281 .Net 201 .phtml 40, 85 .svn-Verzeichnis 259

    : :, Doppelpunkt 68

    _ _(), Funktion 350 __autoload() 67, 407 __call() 406 __clone() 407 __construct() 401, 406 __destruct() 401, 406 __get() 406, 407 __isset() 407 __set() 48, 407 __set_state() 407 __sleep() 407 __toString() 407 __unset() 407 __wakeup() 407 _flashMessage() 163

    _getAuthAdapter() 163 _header.phtml 90 _helper 94 _placeRow.phtml 97 _postDelete() 214 _postUpdate() 214 _reviewFeedback.phtml 124 _reviewItem.phtml 123 _setupDatabase() 146

    < , Element 103 <meta>, Tag 104 , Tag 105<br /> <br /> A Abfrageparser 204 Ablaufdatum 105 Abschnittsvererbung 68 Abstrakte Klasse 404 Access Control List (ACL) 156 Accessibility 63, 91 ACL siehe Access Control List 156 Action 45<br /> <br /> Funktionsname 423 Hilfsklasse 40 Klasse 33 Name 422 action() 99 Action-Controller 313, 319 Action-Hilfsklasse 85<br /> <br /> ACL 171 ActionStack 92<br /> <br /> pushStack() 94 Action-URL 422 Active-Record, Designpattern 137 add() 169<br /> <br /> 433<br /> <br /> Register addActionContext() 119 addAttachment() 238 addBcc() 236 addControllerDirectory() 419 addDocument() 202, 211, 217 addElement() 187 addFilter() 188, 428 addHeader() 227 addModuleDirectory() 419 addRole() 168 addRoute() 424 addToIndex() 216 addValidator() 188 Adobe Systems 363 Adressdaten<br /> <br /> formatieren 123 Ajax 109<br /> <br /> Autovervollständigung 112 Bearbeitung einer Anfrage 118 Beispiel 114 PHP-Validierungscode 114 Benutzeroberfläche 110, 113 check()-Funktion 115 Client-Library 116 Controller-Action 119 Daten auslesen 112 Drag & Drop 113 failure()-Callback 126 feedbackAction() 128 in Webapplikationen 111 in Zend Framework integrieren 118 Informationsfluss 114 JavaScript in View-Skripte einbauen 125 Nachteile 110 startSpinner() 127 stopSpinner() 127 success()-Callback 126 this 126 Validierung von Formularen 112 View-Skriptdatei wählen 119 Vorteile 110 Ajax in Action, Buch 116, 126 ajax.js 115 AjaxContext 118, 128<br /> <br /> Dateinamenendungen 120 format, Parameter 121 Akismet 291, 300<br /> <br /> API-Ressourcen 292 Aktualisierung einer lokalen Arbeitskopie 256 allow() 169 AllowOverride 252<br /> <br /> 434<br /> <br /> Amazon 25, 301<br /> <br /> Affiliate-Programm 310 API-Schlüssel 305 Anfrage 43<br /> <br /> XML-kodierte 288 Anfrageobjekt 88 Animiertes GIF 127 Antwort 45, 118<br /> <br /> Body 46 Code 46 Exception 46 Header 46 Typ 119 Antwortgruppe 307 Anwenderfreundlichkeit 63 Apache 38, 251<br /> <br /> .htaccess 38 konfigurieren 252 mod_rewrite 38 neu starten 252 SetEnv 71 Umgebungsvariable 71 Apache Lucene 200, 201<br /> <br /> Synytax des Abfrageparsers 204 API 298<br /> <br /> Blog 280 MetaWeblog 283 MovableType 283 Schlüssel 300, 311 APP siehe Atom Publishing Protocol 299 appendAlternate() 103 appendScript() 104 Apple Mail 236 application/pdf 383 Applikation<br /> <br /> modulare 420 normale 420 Portabilität 248 Start 71 übergreifende Kommunikation 25 Arbeitskopie 255<br /> <br /> Aktualisieren einer lokalen 256 ARPANET 279 Array 391<br /> <br /> mehrdimensionales 391 array() 391 ArrayAccess 411 ArrayObject 415 assign() 47 asyncRequest() 126 Atom 274, 276<br /> <br /> Register Feed 276 Syndikationsformat 299 Atom Publishing Protocol (APP) 299 Attachment<br /> <br /> anhängen 237 attachObserver() 214 Audioscrobbler 301 Aufgabe<br /> <br /> zeitgesteuerte 210<br /> <br /> B Backslash<br /> <br /> doppelter 389 Balkendiagramm 377 baseUrl() 91, 95 Bedingung<br /> <br /> in PHP 392 Benutzerschnittstelle 62<br /> <br /> Anwenderfreundlichkeit 63 Bilder 63 Features 62 Menüs 62 Navigation 62 Seitenlayout 63<br /> <br /> Aufgabenbereich<br /> <br /> trennen 14 Auflösungsklasse 159 Aufzeichnung<br /> <br /> eines Selenium IDE-Tests 262 Ausdruck<br /> <br /> regulärer 339, 424 Ausloggen 166 auth, Adapter 158 AuthController 161<br /> <br /> Aktualisierung der Controller-Action 185 authenticate() 158 Authentifizierung 155, 156, 286<br /> <br /> Auflösungsklasse 159 Ausloggen 166 Bereich 158 Datenbank 157 Faktoren 156 Hash 164 HTTP-Authentifizierungsprozess 159 Identität 157 Implementierung 157 Nachteile der HTTP-Authentifizierung 160 Salt 164 setCredentialTreatment() 164 Übersicht 24 Zend_Auth_Adapter 157 Zend_Auth_Adapter_Http 159 Authorise, Validator 192 automatic_cleaning_factor, Option 330 automatic_serialization, Option 330 Automatische View<br /> <br /> deaktivieren 277 Automatisierung<br /> <br /> der Selenium IDE-Tests 264 Automattic 302 Automattic Kismet siehe Akismet 300 Autovervollständigung<br /> <br /> mit Ajax 112<br /> <br /> Bereich<br /> <br /> der Authentifizierung 158 Berichtsgenerator 384 Best Practices 248 Beziehung<br /> <br /> many-to-many 150 one-to-many 149 Bild<br /> <br /> in PDF einfügen 381 Binary, Suchfeldtyp 202 Bindestrich<br /> <br /> im URL 423 Block<br /> <br /> verschachtelter 396 Blog-API 280 Blogger 282, 299 Bootstrap 36, 66, 249, 280<br /> <br /> runApp() 356 runXmlRpc() 281 Brainstorming 60 Branch 259 branches/ 253 break() 394 BSD<br /> <br /> Syslog-Protokoll 427 Business-Logik 23, 33, 54, 78<br /> <br /> C Cache<br /> <br /> Optimale Verfallszeit 342 programmatisch leeren 343 cache, Option 338 cache_dir 340 cache_with_cookie_variables 338 cache_with_files_variables 338 cache_with_get_variables 338<br /> <br /> 435<br /> <br /> Register cache_with_post_variables 338 cache_with_session_variables 338 cacheByDefault 334, 336 cachedEntity 336 cachedFunctions 334 cachedMethods 336 Cache-Hit 328, 332 Cache-Miss 328, 332 Cache-Tag 343 Caching 26, 308, 324<br /> <br /> auf verschiedenen Applikationsebenen 342 Cache leeren 343 Cache-Hit 328 Cache-Miss 328 Dateien 336 Datenbankabfragen 328 eigene Identifikatoren wählen 331 eindeutigen Identifikator setzen 331 eindeutiger Identifikator 327 Funktionen 334 Funktionsweise 324 HTML-Output 333 in den Cache aufzunehmen 342 Klassen 335 lifetime 328 Seiten 337 Tags 343 Verfallszeit 327 Vorteile 324 Zend_Cache-Frontends 329 caching, Option 329 CakePHP 16, 29 call() 335 camelCase-Name 422 Cascading Style Sheets (CSS) 35 Case Sensitivity 421 check.ajax.phtml 120 checkDateFormat() 349 Checkout 260 checkUsername() 115 CLA siehe Contributor License Agreement 28 clean() 343 clearDecorators() 196 clearIdentity() 166 Client-Anfrage 288 CMYK, Farbraum 373 Code<br /> <br /> Abdeckung 260 auschecken 254 externer 260<br /> <br /> 436<br /> <br /> planen 64 selbstdokumentierender 406 Wiederverwendbarkeit 14 CodeIgniter 16, 29 Commit<br /> <br /> Regeln 255 Composite-View<br /> <br /> Designpattern 84 Composition over inheritance 368 config 312 config.ini 69, 77, 249 config.ini.default 255 continue() 394 Contributor License Agreement (CLA) 28 Controller 15<br /> <br /> Case Sensitivity 421 Front 71 Funktion 79 Standard 77 URLs 421 Worttrennung 421 Controller-Action 277, 288 Copyright 28<br /> <br /> SCO gegen AutoZone 28 Countable 411 Crane, Dave 116, 126 create() 201, 217 create, read, update und delete siehe CRUD 290 createAttachment() 227 cron 243 Cross-Site Scripting (XSS) 41, 50 CRUD 290<br /> <br /> create, read, update und delete 290 CSS 114, 194, 314 CSS siehe Cascading Style Sheets 35 CSV 349 CSV siehe kommagetrennte Werte 272 CSV, Format 33 current() 411<br /> <br /> D date_created 53, 54, 55 date_default_timezone_set() 37 DATE_LONG, Konstante 360 DATE_SHORT, Konstante 360 date_updated 54 Datei<br /> <br /> cachen 336 statische 39<br /> <br /> Register Daten<br /> <br /> Formatierung 272 loggen 426 serialisierte 272 Übermittlungsmethoden 274 Datenbank 33<br /> <br /> Initialisierung 70 lokale Entwicklungsdatenbank 250 many-to-many-Beziehungen 150 one-to-many-Beziehungen 149 Profiling 428 Unterschiede zwischen Engines 136 unterstützte 133 Verbindungseinstellungen 69 Datenbankabfrage<br /> <br /> cachen 328 Datenbankauthentifizierung 157 Datenbankschema<br /> <br /> anfängliches 72 Datenbanktabelle<br /> <br /> einrichten 72 Datenkapselung 402 Datum<br /> <br /> Format 287 lokalisieren 359 DB2 133 debug_header 338 default_options 338 deg2rad() 380 Dekorator 181 Dekorator, Designpattern 181 DELETE 290, 291 delete() 135, 141 Delicious 301 deny() 169 Deployment<br /> <br /> Apache konfigurieren 252 lokale Hosts-Datei 251 Skripting 267 Versionskontrolle mit Subversion 253 Deployment-Umgebung 249 Design<br /> <br /> Benutzerschnittstelle 62 Designpattern 17, 412<br /> <br /> Active-Record 137 Composite-View 84 Dekorator 181 Factory 51, 132 Factory-Method 187 Front-Controller 32, 33 Model-View-Controller 32, 42, 119, 416<br /> <br /> Controller 33 Diagramm 32 Flussdiagramm 42 Model 33, 51 View 33 MVC 219 Observer 212, 235, 416 Page-Controller 42 Registry 414 Registry 69 Row-Data-Gateway 137, 416 Singleton 38, 85, 157, 412 Strategy 416 Table-Data-Gateway 53, 72, 416 Table-Data-Gateway 137 Two-Step-View 84 Designphilosophie<br /> <br /> Zend Framework 27 Destruktor 401 Development-Umgebung 247 dev-Konfiguration 68 Diagnostik 426 Diagramm<br /> <br /> Balken 377 Tortengrafik 379 DirectoryIterator 409 dirname() 421 Dispatcher 38, 45, 421 Dispatching 32, 44 dispatchLoopShutdown() 47 dispatchLoopStartup() 87, 356 displayAddress() 123 displayDate() 97 Displaygroup 183 Display-Logik 33 Django Python 27 DNS siehe Domain Name System 225 DocBlock 286<br /> <br /> Vorteile 18 Doctrine<br /> <br /> Datenbank-Library 16 doctype() 89, 103 Document Object Model (DOM) 111 document.getElementById() 116 DocumentRoot 252 Dojo 118<br /> <br /> JavaScript-Library 118 Dokument<br /> <br /> in Suchindex aufnehmen 211 Dokumenteigenschaft 366 DOM siehe Document Object Model 111<br /> <br /> 437<br /> <br /> Register Type-Hinting 245 Unterschiede zwischen POP3 und IMAP 241 versenden 227 X-Priority 237 Zend_Mail_Storage_Imap 243 Zend_Mail_Storage_Pop3 243 Zend_Mail_Transport_Sendmail 227<br /> <br /> Domain Name System (DNS) 225 Domäne<br /> <br /> länderspezifische 353 Doppelpunkt 424 Doppelter Backslash 389 Drag & Drop 113 drawEllipse() 380 drawPolygon() 377 dump() 426 Dynamisches Lesezeichen 277<br /> <br /> Entwicklungsdatenbank<br /> <br /> lokale 250 ErrorHandler<br /> <br /> Plug-in 47<br /> <br /> E Eclipse PDT 18 E-Commerce 301 Ecto 283 editPost() 285, 289 Eindeutiger Identifikator 327, 331<br /> <br /> setzen 331 Eingabefeld 115 Einstiegspunkt 47 Einweg-Hash 331 Elementvariable 402 Ellipse 379 Email<br /> <br /> Zend_Validate_EmailAddress 226 E-Mail 26<br /> <br /> Adresse 225 als Datei abspeichern 245 Arbeitsweise 224 Attachments anhängen 237 CC 236 Client 273 Einfügen von Empfängern 236 erstellen 226 Header-Injection 227 HTML-Formatierung 238 IMAP 241 Komponenten einer Adresse 225 lesen 241, 242 mehrere versenden 229 MIME 226 Multipart-Mails lesen 243 POP3 241 Priorität setzen 237 Prioritätshinweise 236 RFC 2822 226 Routing 225 sendmail 227 SMTP 228 speichern 242<br /> <br /> 438<br /> <br /> Erstellungsprozess 61 escape() 41 expires, Meta-Tag 105 Externer Code 260<br /> <br /> F Factory<br /> <br /> Designpattern 51 Factory, Designpattern 132 Factory-Method, Designpattern 187 Faktor<br /> <br /> für Authentifizierung 156 Farbe 373<br /> <br /> Linie 374 Farbraum 373 Favicon 103 Feature<br /> <br /> Benutzerschnittstelle 62 Feed<br /> <br /> erstellen 276 Formate 277 parsen 278 RSS 276 Weiterverarbeitung 278 feedback.json.phtml 129 feedbackAction() 128 Feedbacksystem<br /> <br /> für Rezensionen 122 Fehlerbehandlung 181 Fehlermeldung<br /> <br /> selbst erstellte 189 fetchAll() 78, 139, 236, 325 fetchLatest() 78, 429 fetchNew() 148 fetchRowById() 333 Fielding, Roy 290 file_locking 340 file_name_prefix 341<br /> <br /> Register FILL_METHOD_EVEN_ODD 377 FILL_METHOD_NON_ZERO_WINDING 377 Filter 220<br /> <br /> StripTags 220 verkettete 179 final, Schlüsselwort 403 find() 220 findDependentRowset() 150 findFeeds() 278 findManyToManyRowset() 153 Firefox 261, 295<br /> <br /> dynamische Lesezeichen 277 FlashMessenger 163 Flickr 302, 311 Fluent-Interface 292, 305 Font<br /> <br /> eigener für PDF 370 OpenType 370 Standard für PDF 370 TrueType 370 Footer 89 footer.phtml 89, 90 for() 394 foreach 409 Form<br /> <br /> zeichnen 375 Format<br /> <br /> JPEG 381 PNG 381 TIFF 381 Formular 115<br /> <br /> Dekoratoren 181, 196 Displaygroups 183 eigene Validierungsmeldungen 189 Fehlerbehandlung 181 filtern 188 für Suchläufe 219 HtmlTag, Dekorator 196 Internationalisierung 190 Login 161 Plug-ins 182 rendern 184 selbst erstellte Fehlermeldungen 189 Standarddekoratoren 194 Styling mit CSS 197 Unterformular 183 validieren 188 Fortin, Michel 238 Fowler, Martin 84, 416<br /> <br /> Framework<br /> <br /> lose gekoppeltes 16 Test 74 Fremdschlüssel 151<br /> <br /> mehrere 152 Front-Controller 22, 43, 71<br /> <br /> Modul 418 Plug-in 46, 85 registerPlugin() 93 Front-Controller-Plug-in<br /> <br /> LanguageSetup 355 Registrierung 356 FTP 250, 267 Füllfarbe 373 Funktion 386, 396 Funktionaler Test 260 Funktionsparameter 406 Fußzeile 89<br /> <br /> G GarageSale 272 GD Library 15, 381 Gdata 299, 315 Geistiges Eigentum 28 Geltungsbereich<br /> <br /> globaler 397 Generierung<br /> <br /> von PDF-Berichten 382 GET 290, 291 get() 360 get_object_vars() 130 getConnection() 133 getControllerDirectory() 421 getDate() 349 getDocument() 216 getGraphSection() 377 getIdentity() 160, 166 getInstance() 413, 415 getLanguage() 348, 354 getLastQueryProfile() 429 getMessages() 220 getParam() 357, 423 getQueryProfiles() 430 getRegion() 348 getSearchResultUrl() 221 gettext() 349 getTotalElapsedSecs() 430 getTotalNumQueries() 430 getVideoEntry() 320 getVideoFeed() 317<br /> <br /> 439<br /> <br /> Register namensbasierter virtueller Host 251 virtueller 251<br /> <br /> GIF<br /> <br /> animiertes 127 Globale Variable 69 Globaler Geltungsbereich 397 Google 25, 200<br /> <br /> 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<br /> <br /> API 299, 315 Grad 380 Grafikstatus 379<br /> <br /> speichern 381 Grauskala 373 Gravatar 302 Gruber, John 238<br /> <br /> virtuelles 251 Hostverzeichnis<br /> <br /> virtuelles 252 HTML<br /> <br /> Caching-Output 333 E-Mail 238 strukturiertes 111 HTML-Element<br /> <br /> identifizieren 124 HtmlTag, Dekorator 196 HTTP<br /> <br /> Authentifizierung 157, 158, 160 HTTP-Anfrage 290 httpd.conf 252 httpd.conf, Konfigurationdatei 252 HTTP-Fehler 289 HTTP-Header 277, 383 Hypertext Markup Language siehe HTML 272<br /> <br /> I I18N 347 Identifikator<br /> <br /> eindeutiger 327, 331 Identität 157 Idiom<br /> <br /> H Hash 164 hashed_directory_level 341 hashed_directory_umask 341 hasIdentity() 166 hasPlugin() 93 head*-View-Hilfsklasse 89 Header 89 header.phtml 89 headLink() 88, 95, 103 headMeta() 88, 104 headScript() 104 headTitle() 105 Hello World 36 Heredoc-String 390 Hierarchischer Separator 68 Homepage<br /> <br /> Controller 77 View-Skript 96 Hook 47 Host<br /> <br /> Datei 251 IP-basierter virtueller Host 251<br /> <br /> 440<br /> <br /> Hosting 251<br /> <br /> übersetzen 347 IETF siehe Internet Engineering Task Force 226 iframe 111 IIS 349 imageWithPath() 381 IMAP 223, 241 IMFS siehe Internet Media File System 302 Implementierung<br /> <br /> von Authentifizierung 157 Zugriffskontrolle 167 import() 278 importFile() 279 importString() 279 include() 387 include_once() 387 include_path 35, 37 include-Pfad 77 index.php 71, 250 indexAction() 39, 80, 313 IndexController 39, 78 Indexierung 203<br /> <br /> Register Information Hiding siehe Datenkapselung 402 INI-Datei 250 INI-Format 67 init() 119 Initialisierung 37<br /> <br /> Datenbank 70 innerHTML 116, 127 insert() 135, 140 Instanziierung 400 Integration<br /> <br /> K Kern<br /> <br /> Klassen 23 Kernkomponente<br /> <br /> Übersicht 26 Kettenfilter 179 key() 411 Keyword, Suchfeldtyp 202 Klasse 400<br /> <br /> abstrakte 404 automatisch laden 67 erweitern 403 Präfix 420 Users testen 147<br /> <br /> kontinuierliche 268 Interface 405<br /> <br /> erstellen 283 objektorientiertes 67 Internationalisierung 190, 347<br /> <br /> Übersicht 24 Internet Engineering Task Force (IETF) 226 Internet Explorer 110 Internet Media File System (IMFS) 302 isAllowed() 174 isDispatched() 106 ISO8601<br /> <br /> Format 287 ist ein-Beziehung 403 isValid() 180, 220 isXhtml() 103 Iterator 409<br /> <br /> J<br /> <br /> Klassenname<br /> <br /> Präfix 400 Kommagetrennte Werte (CSV) 272 Kommunikation<br /> <br /> applikationsübergreifende 25 Komponente<br /> <br /> konsistente 27 Komposition<br /> <br /> Vererbung 368 Konfiguration 67<br /> <br /> Vererbung 250 von Apache 252 Konfigurationsabschnitt 249 Konflikt<br /> <br /> bei Bearbeitungen 257<br /> <br /> Java 201 JavaScript 104, 111, 120<br /> <br /> Ajax 109 Callback-Funktion 114 Client-Library 116 Konstruktorfunktion 126 Objektmodell 126 prototype 126 ReviewFeedback, Klasse 126 testen 267 Wahl einer Library 117 JavaScript Object Notation (JSON) 25 JOIN 149 JPEG 381 jQuery 118 JSON 102, 111, 272<br /> <br /> .json.phtml 120 JSON siehe JavaScript Object Notation 25<br /> <br /> Konsistenz 27 Konstruktor 401 Kontinuierliche Integration 268 Kopfzeile 89 Kopplung 69, 414 Kreise 379<br /> <br /> L Länderspezifische Domäne 353 LanguageSetup 355 Last in, first out (LIFO) 92 Last.fm 301 Laufzeitinstanziierung 400 Layout<br /> <br /> abschalten 118 Datei 307 Rendern deaktivieren 383 layout() 90 layoutPath 86 Layout-Rendering<br /> <br /> deaktivieren 277<br /> <br /> 441<br /> <br /> Register LDAP siehe Lightweight Directory Access Protocol 156 Lesezeichen<br /> <br /> dynamisches 277 Library<br /> <br /> GD 381 lifetime, Option 329 lifetime, Schlüssel 328 LIFO siehe Last in, first out 92 Lightweight Directory Access Protocol (LDAP) 156 Linie<br /> <br /> gestrichelte 375 gezeichnete 375 horizontale 375 Linienfarbe 374 Link-Tabelle 151 listAction() 319 Live Bookmark 277 live-Konfiguration 68 Lizenz 312 Locale 347 localhost 251 log() 427 LoggedInUser, View-Hilfsklasse 165 Logging<br /> <br /> ausschalten 428 Filter 428 Level 428 logging, Option 329 Login-Formular 161 Lokale Arbeitskopie<br /> <br /> aktualisieren 256 Lokalisierung 347<br /> <br /> Datum und Zeit 349 Formatierung von Zahlen 348 Normalisierung 348 Lose gekoppeltes Framework 16<br /> <br /> M<br /> <br /> Punkt 365 master_file 337 Master-Layout-Skript 88 MaxBufferedDocs 209 MD5 164 md5() 331 MD5-Algorithmus 331 Metainformation 365 Meta-Tag 104<br /> <br /> expires 105 Metaweblog 282 MetaWeblog<br /> <br /> API 283 Methode 400<br /> <br /> magische 406 statische 335 Microsoft Outlook 236 MIME siehe Multipurpose Internet Mail Extensions 226 Mindmap 60 Miniapplikation 418 MixedCase-Name 422 mod_rewrite 251 Model 15, 51, 313, 328, 367<br /> <br /> Klasse 285 Test 74, 78 testen 144 Unit-Test 74 Model-View-Controller<br /> <br /> Designpattern 119 Model-View-Controller (MVC) 13<br /> <br /> Controller 15 Model 15 Überblick 22 View 15 Modul 418<br /> <br /> Mac OS X<br /> <br /> Ecto 283 Magische Methode 406 Mail User Agent (MUA) 224 mail() 224 Mailserver 273 make_id_with_cookie_variables 339 make_id_with_files_variables 339 make_id_with_get_variables 338 make_id_with_post_variables 339<br /> <br /> 442<br /> <br /> Many-to-many-Beziehung 150 Markdown 243 Mashup 297 Maßeinheit<br /> <br /> Standard 418 URL-Schema 419 Modulare Applikation 420 MooTools 118 MovableType<br /> <br /> API 283 move_uploaded_file() 237 MTA 273 MUA 273 MUA siehe Mail User Agent 224 Multipart-Mail 243<br /> <br /> Register Multipurpose Internet Mail Extensions (MIME) 226 MVC 64<br /> <br /> One-to-many-Beziehung 149 onkeyup 115 OOP<br /> <br /> Miniapplikationen 418 Separation 120 Tipps und Tricks 418 Verzeichnisstruktur 418 MVC siehe Model-View-Controller 13, 31 MVC, Designpattern 416 MVC, Designpattern 219 MySQL 23, 67, 133 mysql_real_escape_string() 52<br /> <br /> N Nachricht<br /> <br /> Priorität 427 Namenskonvention 367 NameVirtualHost 252 Neuindexierung 218 new, Schlüsselwort 400 next() 411 Nirvanix 302 NNTP 223 nonCachedFunctions 334 nonCachedMethods 336 Normalisierung 348 notifyObservers() 214 Nowdoc-String 390 Numerischer Schlüssel 425 Nutzungsbedingung 315<br /> <br /> O Objekt 400<br /> <br /> beobachtbares 212 drehen 380 konstruieren 401 löschen 401 partielle Schleife 102 Schnittmasken zeichnen 382 Objektorientierte Programmierung (OOP) 400 Objektorientiertes Interface 67 Objektorientiertes Programmieren (OOP) 15 Objektschnittstelle 284 Observer, Designpattern 212, 416 Observer, Designpattern 235 observeTableRow() 214, 215 offsetExists() 411 offsetGet() 411 offsetSet() 411 offsetUnset() 411<br /> <br /> Abstraktion 404 Elementvariablen 402 Erweiterung von Klassen 403 Interface 405 ist ein-Beziehung 403 Sichtbarkeit 401 Vererbung 403 OOP siehe objektorientierte Programmierung 400 OOP siehe objektorientiertes Programmieren 15 Open Office 303 open() 217 OpenID 156 OpenType 370 Operator 204 optimize() 203 Oracle 23, 133<br /> <br /> P Pages_IndexController 418 Parameter<br /> <br /> durchgereichter 69 partial()<br /> <br /> variabler Geltungsbereich 90 View-Hilfsklasse 89 partialLoop() 95, 101, 123 Partielles View-Skript 90 Pascarello, Eric 116, 126 Passwort<br /> <br /> Hash 164 Pattern<br /> <br /> Factory 51 Front-Controller 22, 32, 33 Model-View-Controller 32, 42 Controller 33 Diagramm 32 Flussdiagramm 42 Model 33, 51 View 33 Model-View-Controller 15 Page-Controller 42 Singleton 38 Table-Data-Gateway 23, 53 PDF 26, 303, 363, 364<br /> <br /> Author 366 Berichte generieren 382 Berichtsgenerator 367, 384<br /> <br /> 443<br /> <br /> Register Bilder einfügen 381 CreationDate 366 Creator 366 Dokumenteigenschaften 366 Erstellen und Laden 364 Fonts wählen 370 Füllfarbe 373 gestrichelte Linien 375 Grafikstatus 371 Größe und Ausrichtung einer Seite 365 Keywords 366 Kreise und Ellipsen 379 Linien zeichnen 375 Linienfarbe 374 Maßangaben 365 ModDate 366 Objekte drehen 380 Producer 366 Rechtecke und Polygone 377 Seiten erstellen 364 Seitengröße 365 Size_Letter 369 speichern 367 Standard-Fonts 370 Styles 374 Subject 366 Text 370 Textumbruch 371 Titel-Eigenschaft 366 Title 366 Trapped 366 PDF-Dokument<br /> <br /> Seitengröße SIZE_A4 365 SIZE_A4_LANDSCAPE 365 SIZE_LETTER 365 SIZE_LETTER_LANDSCAPE 365 PEAR 16 Pfad<br /> <br /> umgebungsspezifischer 249 PHP 223<br /> <br /> Farbe 373 Frameworks 29 Funktion 386 Grundlagen 386 endwhile 396 endfor 396 endforeach 396 endswitch 396 Alternative Syntax für verschachtelten Block 396<br /> <br /> 444<br /> <br /> array() 391 Arrays 391 Bedingung 392 boolean 387 break 393, 394 continue 394 do-while 393 else() 392 elseif() 392 endif 396 false 387 float 387 for() 394 foreach() 395 Funktion 396 Funktionen benennen 397 Heredoc-Strings 390 if() 392 if, Anweisung 392 include() 387 int 387 mehrdimensionales Array 391 Nowdoc-Strings 390 null 388 object 388 Referenz 395 require() 387 resource 388 return 397 Schleife 393 Skript starten 386 string 387 Strings in doppelten Anführungszeichen 389 Strings in einfachen Anführungszeichen 389 switch() 393 switch, Anweisung 392 true 387 Typ 387 Type Juggling 387 Typveränderung 387 Variablen und Typen 387 variabler Geltungsbereich in Funktionen 397 while() 393 Markdown 238 Syntax 386 Website-Struktur 14 PHP5<br /> <br /> neue Features 403 PHPTAL 22, 33 PHPUnit 74, 261<br /> <br /> Installation 74<br /> <br /> Register starten 75 Testfall 75 Unit-Test 265 Picasa Web Albums 299 Places<br /> <br /> ACL preDispatch() 173 Ajax-Integration 121 Amazon-Model-Klasse 305 andere Sprache einfügen 351 AuthController 161 Benutzerschnittstelle 62 Bootstrap-Klasse 66 Business-Logik 78 Code planen 64 Controller 79 Datenbankschema 72 Dokument in Suchindex aufnehmen 211 Einloggen 161 Einrichtung der Zugriffskontrolle in Controller-init() 174 E-Mails integrieren 230 erstes Model 72 Feedbacksystem für Rezensionen 122 fetchLatest(), Modelfunktion 78 Flickr integrieren 313 Flickr-Model-Klasse 311 Gestaltung des Suchindex 210 Homepage 71 Homepage-Design 64 indexAction 80 Konfigurationsdatei 69 menuAction() 94 Mindmap 60 neue Weiterleitungsregel implementieren 354 PDF-Bericht rendern 382 PDF-Berichtsgenerator 367 Place-Controller 122 PLACES_CONFIG 71 Places_Controller_Action_Helper_Acl 171 render() 382 Report_Document 367 Report_Page 368 ReportController 383 REST-Server 295 reviews-Table 151 sendMail() 235 Service_Places-Klasse 294 Skelettstruktur 65<br /> <br /> Sprache auf Locale zuordnen 353 Story 60 Suchfunktion einfügen 210 Support, Model 231 Support_Mailer, Klasse 234 Support_Table 231 SupportController 233 Support-Tracker 231 Tests 74 User 143 Users-Model 142 Verzeichnis für View-Hilfsklassen 88 Verzeichnisstruktur 65 YouTube integrieren 316 Zend_Layout integrieren 86 Ziel der Site 60 Zugriffskontrolle 169 Places.RestController 295 Places_Controller_Action_Helper_Acl 171 Places_Controller_Plugin_ActionSetup 92 Places_Controller_Plugin_LanguageSetup 355<br /> <br /> dispatchLoopStartup() 356 Places_Controller_Plugin_ModelDirSetup 420 Places_Controller_Plugin_ViewSetup 87, 106<br /> <br /> postDispatch() 106 Places_Db_Table_Row_Observable 213 Places_Search_Lucene 216<br /> <br /> addDocument() 217 create() 217 open() 217 Places_Search_Lucene_Document 212, 221 Places_Validate_Authorise 186 Places_View_Helper_BaseUrl 91 Platzhalter 85, 204<br /> <br /> Standard 424 Variable 424 Plug-in<br /> <br /> Front-Controller 46 für Formulare 182 PNG 381 poEdit 351 Polygon 377 POP3 223, 241 POP3 siehe Post Office Protocol 224 Portabilität 248 Portable Document Format (PDF) 363 POST 290, 291 Post Office Protocol (POP3) 224 postDispatch() 47 PostgreSQL 23, 133 postInsert() 214<br /> <br /> 445<br /> <br /> Register postUpdate() 214 PowerPoint 303 Präfix 400 preDispatch() 47, 119, 173 prependAlternate() 103 prependStylesheet() 103 printf()-Platzhalter 350 Priorität<br /> <br /> von Nachrichten 427 private 402 Privileg 167, 169 Production-Umgebung 247, 248 Produktionsserver 37 Profiling<br /> <br /> getLastQueryProfile() 429 Programmierung 65 Propel 35 protected 402 Protokoll 274 Prototype 118 prototype, Eigenschaft 126 public 402 Punkt<br /> <br /> konvertieren 365 pushStack() 94 PUT 290, 291<br /> <br /> Q Qt 349 query() 129, 133 quote() 133 quoteInto() 135<br /> <br /> R Radiant 380 Rails 20, 27 Rand<br /> <br /> linker 369 Rangliste<br /> <br /> der Suchresultate 200 RBACL siehe Rollenbasiertes Zugriffskontrollsystem 24 RDF 276 read_control 340 read_control_type 340 readMail() 245 Rechteck 377 Refaktorierung 35, 74 Refakturierung 234, 236 refTableClass 150<br /> <br /> 446<br /> <br /> regexps 339 regex-Route 425 registerAutoload() 67 registerPlugin() 93 Registry 69 Registry, Designpattern 414 Regulärer Ausdruck 339 Regulärer Ausdruck 424 reindexAction() 218 Remember The Milk 303 Remote Procedure Call (RPC) 273 remove() 343 render() 367 Rendern 90 Repository<br /> <br /> erstellen 253 Representational State Transfer (REST) 290 Request for Comments (RFC) 279 require() 387 require_once() 387 Response Code<br /> <br /> 200 46 302 46 404 46 Ressource 24, 156, 167, 168, 290 REST siehe Representational State Transfer 290 restPost() 293 REST-Ressource 290 REST-Server 295 review_feedback.js 125 ReviewController<br /> <br /> feedbackAction() 128 rewind() 411 RewriteCond 39 rewrite-Router 423 RewriteRule 39 Rezension<br /> <br /> Feedbacksystem 122 RFC siehe Request for Comments 279 RGB, Farbraum 373 Richtlinie des gleichen Ursprungs 264 Rolle 24, 167, 168 Rollenbasierte Zugriffskontrollliste 167 Rollenbasiertes Zugriffkontrollsystem 24 ROOT_DIR 146, 356 rotate() 380 Route<br /> <br /> Objekt 423 regex 425 Standard 423<br /> <br /> Register Standardeinstellungen 424 statische 425 Template 424 Router 43<br /> <br /> rewrite 423 Standard 419, 423 routeShutdown() 47 routeStartup() 47 Routing 32, 44, 423<br /> <br /> neue Weiterleitungsregel implementieren 354 numerische Schlüssel 425 regex-Route 425 statische Route 425 Row-Data-Gateway, Designpattern 137, 416 Rowset 277 RPC<br /> <br /> Middleware 280 zentrales Element 290 RPC siehe Remote Procedure Call 273 RSS-Feed 276<br /> <br /> parsen 278 Weiterverarbeitung 278 Ruby on Rails 185 Ruby on Rails siehe Rails 20 runApp() 356 runXmlRpc() 281<br /> <br /> S Salt 164 save() 140 saveIssue() 232 saveXml() 277 Schleife<br /> <br /> in PHP 393 Schlüssel<br /> <br /> numerischer 425 zusammengesetzter 150 Schlüsselwort 307, 313, 400 Schnittmaske 382 Schnitttabellenklasse 152 score 222 SDN siehe Storage Delivery Network 302 SearchIndexer 214<br /> <br /> _addToIndex() 216 getDocument() 216 observeTableRow() 215 Seite<br /> <br /> cachen 337 drehen 381<br /> <br /> Größe 369 Verzögerung im Aufbau 308 Seitentitel 106 Selenium IDE 261<br /> <br /> Aufzeichnung eines Tests 262 Automatisierung der Tests 264 Bearbeitung des Tests 262 PHPUnit-Testfall 264 Quellcode 263 Speichern des Tests 263 Test Runner 264 Selenium RC 264<br /> <br /> Server 265 send() 277 Separation of concerns 14 Separator<br /> <br /> Doppelpunkt 68 hierarchischer 68 serialize() 272, 331 Server 267<br /> <br /> einrichten 247 setAction() 187 setBodyHtml() 239 setBodyText() 239 setControllerDirectory() 419 setCredentialTreatment() 164 setDecorators() 196 setDefaultTransport() 229 setElementDecorators() 196 setLayout() 86 setLineDashingPattern() 376 setlocale() 347<br /> <br /> Probleme 360 setMessage() 189 setRequired() 188 setSlop() 207 setUp() 144 setView() 165 SGML siehe Standard Generalized Markup Language 272 SHA1 164 sha1() 331 SHA1-Algorithmus 331 SHAPE_DRAW_FILL 377 SHAPE_DRAW_FILL_AND_STROKE 377 SHAPE_DRAW_STROKE 377 Sicherheit 23, 286<br /> <br /> E-Mail-Header-Injection 227 SQL Injection 52, 133 SQL Injection-Schwachstelle 129 XSS 48, 50<br /> <br /> 447<br /> <br /> Register Simple Mail Transfer Protocol (SMTP) 228 Simpy 303 Singleton 85<br /> <br /> Designpattern 38, 157 Skelettstruktur 65 Skripting<br /> <br /> des Deployments 267 SlideShare 303 Smarty 22, 33<br /> <br /> Template-Engine 16 SMTP siehe Simple Mail Transfer Protocol 228 SOAP 25, 273 Software<br /> <br /> Designpattern 412 Solar 29 Spamfilter 291 Speichern<br /> <br /> von E-Mails 242 Spinner<br /> <br /> Animation 125 erstellen 127 SPL<br /> <br /> ArrayAccess 411 Countable 411 DirectoryIterator 409 Iterator 409 SPL siehe Standard PHP Library 67, 409 SQL 73, 77, 129<br /> <br /> Abfrage 133 Injection-Schwachstelle 129 join 149 Platzhalter 133 SQL Injection 52, 133 SQL Server 23, 133 SQLite 23 Staging-Umgebung 247, 248 Standard Generalized Markup Language (SGML) 272 Standard PHP Library (SPL) 67, 409 Standard-Controller 77 Standard-Dispatcher 45 Standard-Modul 418 Standardroute<br /> <br /> ersetzen 354 Standard-Route 423 Standard-Router 419 start() 333, 337 Statische Datei 39 Statische Methode 335 Statische Route 425<br /> <br /> 448<br /> <br /> Statische Variable 413 Storage Delivery Network (SDN) 302 Story<br /> <br /> für Website 60 Strategy, Designpattern 416 StrikeIron 303 String 388<br /> <br /> Heredoc 390 in doppelten Anführungszeichen 389 in einfachen Anführungszeichen 389 Nowdoc 390 string, Typ 388 StripTags, Filter 220 strtok() 372 Struts 20 Styling mit CSS 197 Subversion 253<br /> <br /> Arbeit mit Branches 259 Arbeitskopie 255 Code auschecken 254 committen 255 export 259 externer Code 259, 260 Kopie 259 propedit 260 resolved 258 Status 256 Umgang mit Konflikten 257 update 256 Verzeichnisstruktur 254 Suchformular 219 Suchfunktion<br /> <br /> Abfrage 204 Abfragen des Index 203 Abfrageobjekte 206 Abfrageparser 204 API-Abfrage 206 Aufteilung in Tokens 203 Begriff 204 Begriffsabfrage 206 Bereichsabfragen 208 Beziehung zwischen Index, Dokumenten und Feldern 201 Boolesche Operatoren 205 Einsatz von Feldtypen 203 Ergebnisse darstellen 221 Feldtypen 202 in Index aufnehmen 210, 212 Indexierung 203 mehrfache Begriffsabfrage 206 Modifikatoren 205<br /> <br /> Register neuen Index erstellen 218 Neuindexierung 218 Operator 204 Phrasenabfragen 207 Platzhalter 204 score 222 Slop 207 String-Abfragen 204 Suchformular 219 Suchindex designen 210 Verarbeitung von Suchanfragen 219 Volltextsuchmaschine 201 Vorteile 199 Wildcard-Abfragen 207 zur Darstellung verfügbare Felder 221 Suchmaschine<br /> <br /> Apache Lucene 26 Suchresultat<br /> <br /> Rangliste 200 Sun 84 Support<br /> <br /> readMail() 245 saveIssue() 232 Support_Table 231 svn 253<br /> <br /> commit 258 export 259 import 254 mkdir 254 propedit 260 resolved 258 status 258 Status 256 update 256, 267 svnadmin, Befehl 253 Symfony 16, 29 Syntax<br /> <br /> für verschachtelten Block 396<br /> <br /> T Tabelle<br /> <br /> Beziehungen 149 Link-Tabelle 151 Schnittstellenklasse 152 verlinken 149 Zielorttabelle 152 Table-Data-Gateway<br /> <br /> Designpattern 53, 72 Table-Data-Gateway, Designpattern 137, 416 Tabulatorgetrennte Werte (TSV) 272<br /> <br /> tags/ 253 tagSearch() 312 TBX 349 Teil-View-Skript 124 Template 33<br /> <br /> Action 84 Master 84, 85 Route 424 Test<br /> <br /> Abschnitt 69 automatischer 69 Daten 79 Framework 74 funktionaler 260 JavaScript 267 mit Selenium IDE 261 Model 144 Models 74 Testsuite 267 Zend_Http_Client 265 TestConfiguration 76<br /> <br /> Klasse 75 Testen<br /> <br /> auf die Schnelle 426 Testfall<br /> <br /> Klasse 80 PHPUnit 75 testInsert() 148 tests-Verzeichnis 80 testUpdate() 148 Text 111<br /> <br /> strukturierter 272 Text, Suchfeldtyp 202 Textumbruch 371 TIFF 381 Titel<br /> <br /> Separator 106 TitleCase 44 toArray() 101 Token<br /> <br /> Aufteilung in 203 Tokenizing 203 toNumber() 348 Tortendiagramm 379 translate() 357 TrueType 370 trunk/ 253 TSV siehe tabulatorgetrennte Werte 272 Two-Step-View<br /> <br /> Designpattern 84 Typ 387<br /> <br /> 449<br /> <br /> Register Type Hinting 405 Type Juggling 387 Type-Hinting 245 TypeKey 156 Typveränderung 387<br /> <br /> testen 147<br /> <br /> V<br /> <br /> U Übersetzung<br /> <br /> Dateien 358 Idiome 347 View 357 von Websites 346 Zend_Translate 349 UI siehe User Interface 62 UI-Widget 117 Umgebung 37<br /> <br /> einrichten 247 Umgebungsvariable 71 UnIndexed, Suchfeldtyp 202 Unit-Test 35, 74<br /> <br /> _setupDatabase() 146 AllTests.php 76 Controller 80 Controller-setUp() 81 Model 144, 147 Model-Initialisierung 145 Models 74 Prozess 61 setUp() 75, 144 Strukturierung 76 tearDown() 75 Testfall 75 testInsert() 148 testUpdate() 148 Verzeichnisorganisation 76 Unstored, Suchfeldtyp 202 Unterformular 183 Update<br /> <br /> in place 122 update() 140 URL<br /> <br /> Action-URLs 422 Bindestrich 423 Controller 421 generieren 99 lesefreundlicher 43 Schema 419 url() 99, 358 url(), View-Hilfsklasse 162, 420 urlencode 100<br /> <br /> 450<br /> <br /> User 143 User Interface 62 Users-Klasse<br /> <br /> valid() 411 Validator<br /> <br /> Authorise 192 selbst erstellter 192 Validierung 23, 160 Variable 387, 400<br /> <br /> globale 69 Platzhalter 424 statische 413 Variable 388 Variablen<br /> <br /> Parsing in Strings 389 Vererbung 400, 403 Verfallszeit 327<br /> <br /> des Caches 342 Versionskontrolle 253 Verzeichnis<br /> <br /> branches/ 253 für View-Hilfsklassen 88 tags/ 253 tests 80 trunk/ 253 virtuelles Hostverzeichnis 252 zentrales 65 Verzeichnisstruktur 34, 65, 254<br /> <br /> application 34 library 35 public 35 tests 35 Top Level 34 Video 316<br /> <br /> abspielen 320 Kategorien 317 VideosController<br /> <br /> viewAction() 320 View 15<br /> <br /> Datei 321 Hilfsfunktionen 49 Hilfsklasse 33 JavaScript einbauen 125 Master-Template 85 Rendern deaktivieren 383 Skript 33, 48 übersetzen 357<br /> <br /> Register Darstellen von Flickr-Bildern 311 Einsatzgebiete 275 Lizenz 312 Nutzungsbedingungen 316 Übersicht 25 YouTube-Beispiel 316 YouTube-Kategorien 317<br /> <br /> View-Hilfsklasse 307<br /> <br /> action() 99 amazonAds 307 baseUrl() 91 Beachtung von Groß/Kleinschreibung 98 Benennung 307 displayAddress() 123 displayDate() 97 doctype() 103 für HTML-Kopfzeilen 102 getSearchResultUrl() 221 head* 89 headLink() 103 headMeta() 104 headScript() 104 headTitle() 105 Integration von Controllern 99 json() 102 layout() 90 LoggedInUser 165 partial() 89, 101 partialLoop() 101, 123 setView() 165 translate() 357 url() 99, 162, 358, 420 ViewRenderer 40, 85 ViewSetup 87 View-Skript<br /> <br /> partielles 90 verwalten 101 VirtualHost 252 Virtueller Host 251<br /> <br /> IP-basierter 251 namensbasierter 251 Virtuelles Hosting 251 Virtuelles Hostverzeichnis 252 Volltextsuchmaschine 201<br /> <br /> 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<br /> <br /> Amazon-Beispiel 305 Arbeitsweise 274 Caching 308<br /> <br /> Website<br /> <br /> Aufgabe der Site Places 60 Benutzerschnittstelle 62 Planung 60 Spezifikation 60 Story 60 übersetzen 346 Werbeanzeige 305 Wert<br /> <br /> komma- oder tabulatorgetrennter 272 while() 393 Wiederverwendbarkeit 14 Windows 347<br /> <br /> 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<br /> <br /> X X_REQUESTED_WITH 121 XLIFF 349 XML 111, 272, 277<br /> <br /> .xml.phtml 120 kodierte RPCs 279 XMLHttpRequest 110, 118 XML-kodierte Anfrage 288 XML-RPC 25, 273<br /> <br /> Anfragen 280 Client-Komponente 274 Diagramm 274 Erstellen der Interfaces 283 Methoden-Handler 282 Namensraum 282 Server 275, 280 XMLRPC_TYPE_DATETIME 287 XML-RPC-Fehler 289<br /> <br /> 451<br /> <br /> Register XmlTm 349 X-Priority 237 XSS siehe Cross-Site Scripting 41 XSS-Schwachstelle 48<br /> <br /> Y Yahoo! 25, 304<br /> <br /> Flickr-Fotodaten 25 Yahoo! Images 304 Yahoo! Local 304 Yahoo! News 304 Yahoo! User Interface (YUI) 117 Yahoo! Web Search 304 YAHOO.util.Connect 117 YouTube 299<br /> <br /> Abspiellisten 317 API 317 durch Abspielliste iterieren 317 Video abspielen 320 Videoliste 319 YUI<br /> <br /> Connect-Objekt 121 Integration 117 YUI-Library-Datei 117<br /> <br /> Z Zahl<br /> <br /> regionale Probleme 348 Zeichen 354<br /> <br /> für Worttrennung 421 Zeitgesteuerte Aufgabe 210 Zend Framework 29<br /> <br /> Ajax integrieren 118 Anforderungen 248 build-tools-Verzeichnis 267 Code-Qualität 27 Definition 20 Designphilosophie 27 einfachere Entwicklung 19 Einfachheit 28 Einsatzgründe 16 Features 16 geistiges Eigentumsrecht 28 Geschichte 20 Kernklassen 23 Klassenkategorien 16, 21 Komponenten 21 leicht zu erlernen 17 Liste der Komponenten 21 modernes Design 17<br /> <br /> 452<br /> <br /> MVC-Flowchart 22 Namenskonventionen 367 Online-Manual 18 schnelle Entwicklung 19 Struktur 19 Tutorials 18 vollständige Dokumentation 18 Wartung und Pflege 19 Zend Framework 1.5 178 Zend Technologies 21, 28 Zend_Acl 24, 168<br /> <br /> add() 169 allow() 169 deny() 169 Konfiguration 170 Vererbung von Privilegien 169 Zend_Acl_Resource 168 Zend_Acl_Role 168, 170<br /> <br /> addRole() 168 Zend_Auth 24, 157, 162<br /> <br /> authenticate() 158 clearIdentity() 166 Diagramm des HTTPAuthentifizierungsprozesses 159 getIdentity() 166 hasIdentity() 166 HTTP authentication flowchart 159 Zend_Auth_Adapter 157, 158 Zend_Auth_Adapter_DbTable 163 Zend_Auth_Adapter_Http 159<br /> <br /> getIdentity() 160 Zend_Cache 26, 308<br /> <br /> Backend-Klassen 340 Beispiel 309, 325 CLEANING_MODE_ALL 343 CLEANING_MODE_MATCHING_TAG 343 CLEANING_MODE_OLD 343 Datenbankabfragen cachen 328 Frontends 329 implementieren 328 lifetime 328 Webservices integrieren 309 Zend_Cache _Frontend_Class_Backend_ZendPlatform 330 Zend_Cache_ Backend_MemcachedFrontend_Output 330 Zend_Cache_ Frontend_FunctionBackend_Apc 330 Zend_Cache_Backend_Apc 341<br /> <br /> Register Zend_Cache_Backend_File 326, 340<br /> <br /> cache_dir 329, 340 file_locking 340 file_name_prefix 341 hashed_directory_level 329, 341 hashed_directory_umask 341 Optionen 341 read_control 340 read_control_type 340 Zend_Cache_Backend_Memcached 341 Zend_Cache_Backend_Sqlite 341 Zend_Cache_Backend_ZendPlatform 341 Zend_Cache_Core 326, 329<br /> <br /> clean() 343 remove() 343 Zend_Cache_Coreache_Backend_Sqlite 330 Zend_Cache_Frontend_Class 335<br /> <br /> cacheByDefault 336 cachedEntity 336 cachedMethods 336 nonCachedMethods 336 Verwendungsbeispiel 336 Zend_Cache_Frontend_File 330, 336<br /> <br /> master_file 337 Verwendungsbeispiel 337 Zend_Cache_Frontend_Function 334<br /> <br /> cacheByDefault 334 cachedFunctions 334 call() 335 nonCachedFunctions 334 Optionen 334 statische Funktionen 335 Verwendungsbeispiel 335 Zend_Cache_Frontend_Output 333<br /> <br /> start() 333 Zend_Cache_Frontend_Page 330, 337<br /> <br /> debug_header 338 default_options 338 regexps 339 start() 337 Verwendungsbeispiel 339 Zend_Config 67, 306, 408<br /> <br /> Abschnittsvererbung 68 Datenzugriff 68 get() 408 hierarchischer Separator 68 laden 68 Testabschnitt 69 Zend_Config_Ini 249<br /> <br /> Vererbung 250 Zend_Controller_Action 45<br /> <br /> _helper 94 init() 45 postDispatch() 45 preDispatch() 45 Zend_Controller_Action_ViewRenderer 40 Zend_Controller_Dispatcher_Standard 44 Zend_Controller_Front 33, 38, 43<br /> <br /> addControllerDirectory() 419 addModuleDirectory() 419 dispatch() 38 getControllerDirectory() 421 renderExceptions() 46 Singleton-Implementierung 412 Zend_Controller_Plugin_ActionStack 92 Zend_Controller_Request_Http 43<br /> <br /> getParam() 43 Zend_Controller_Response_Cli 45 Zend_Controller_Response_Http 45 Zend_Controller_Router_Rewrite 44, 423 Zend_Controller_Router_Route 354, 424 Zend_Controller_Router_Route_Regex 425 Zend_Controller_Router_Route_Static 425 Zend_Currency 25 Zend_Date 98, 349, 360, 379<br /> <br /> add() 105 get() 105 Konstanten 360 Zend_Db 51<br /> <br /> factory() 51, 70, 132 getConnection() 133 params-Abschnitt 429 Profiling 428 query() 133 quote() 52, 133 Sicherheit 52 Zend_Db_Adapter 70, 129, 131<br /> <br /> delete() 135 erstellen 132 insert() 135 query() 129 quoteInto() 135 update() 135 Zend_Db_Adapter_Pdo_Mysql 70 Zend_Db_Expr 136 Zend_Db_Profiler 428<br /> <br /> getLastQueryProfile() 429 getTotalElapsedSecs() 430 getTotalNumQueries() 430 über config-Objekt aktivieren 428 Zend_Db_Profiler_Query 430 Zend_Db_Select 134<br /> <br /> 453<br /> <br /> Register Zend_Db_Table 23, 33, 53, 136, 137, 138, 231<br /> <br /> Beziehungen 149 delete() 141 erweitern 53 fetchAll() 78, 236 insert() 139 integration 142 save() überschreiben 234 setDefaultAdapter() 53 update() 140 Zend_Db_Table_Abstract 137, 431<br /> <br /> $_dependantTables 149 $_name 138 $_referenceMap 149, 152 $_rowClass 143 fetchAll() 139 fetchNew() 148 findDependentRowset() 150 findManyToManyRowset() 152, 153 Zend_Db_Table_Row 137<br /> <br /> save() 140 Zend_Db_Table_Row_Abstract 143, 214<br /> <br /> _postDelete() 214 _postInsert() 214 _postUpdate() 214 Zend_Db_Table_Rowset 137 Zend_Db_Table_Rowset_Abstract 410 Zend_Db_Table_Select 139 Zend_Debug 426<br /> <br /> dump() 37, 426 Zend_Feed 25, 276<br /> <br /> findfeeds() 278 import() 278 importFile() 279 importString() 279 saveXml() 277 send() 277 Zend_Filter 23, 178<br /> <br /> Filter 179 Liste der Klassen 178 Zend_Filter_Input 220<br /> <br /> isValid() 220 Zend_Form 178, 186, 233<br /> <br /> addElement() 187 clearDecorators() 196 eigene Dekoratoren setzen 194 Filtern 188 init() 187 Internationalisierung 190 populate() 186 rendern 184<br /> <br /> 454<br /> <br /> selbst erstellte Fehlermeldungen 189 selbst erstellte Validatoren 192 setAction() 187 setDecorators() 196 setElementDecorators() 196 setTranslator() 191 Standarddekoratoren 181, 194 Validierung 188 Vorteile 178 Zend_Form_Element<br /> <br /> addFilter() 188 addValidator() 188 setRequired() 188 Zend_Gdata 298, 315<br /> <br /> getVideoEntry() 320 Zend_Http_Client 265, 292, 293 Zend_Json 25, 102 Zend_Layout 22, 85<br /> <br /> integrieren 86 setLayout() 86 startMvc() 85 Zend_Loader 67<br /> <br /> loadClass() 37 Zend_Locale 25, 347<br /> <br /> checkDateFormat() 349 Datum und Zeit 349 getDate() 349 getLanguage() 348, 354 getRegion() 348 toNumber() 348 Zend_Log 426, 429<br /> <br /> addFilter() 428 log() 427 Shortcut-Methode 427 Writer-Objekt 427 Zend_Log_Filter_Priority 428 Zend_Log_Writer_Db 428 Zend_Log_Writer_Firebug 428 Zend_Log_Writer_Null 428 Zend_Log_Writer_Stream 428 Zend_Mail 26, 225<br /> <br /> addAttachment() 238 addBcc() 236 addCC() 236 addHeader() 227, 237 createAttachment() 227 Einfügen von Empfängern 236 E-Mails erstellen 226 E-Mails lesen 242 E-Mails versenden 227 Integration in Places 234<br /> <br /> Register Maildir 242 Mbox 242 Priorität setzen 237 Prozess 224 setBodyHtml() 239, 240 setBodyText() 239, 240 setDefaultTransport() 229 über SMTP versenden 229 Verbindung zum Speichern öffnen 242 Versand mit SMTP 229 View zum Rendern 240 Zend_Mail_Storage<br /> <br /> Nachrichtentext auslesen 243 Zend_Mail_Storage_Abstract 243 Zend_Mail_Storage_Imap 243 Zend_Mail_Storage_Maildir 242 Zend_Mail_Storage_Mbox 242 Zend_Mail_Storage_Pop3 243 Zend_Mail_Transport_Interface 26 Zend_Mail_Transport_Sendmail 227 Zend_Mail_Transport_Smtp<br /> <br /> Vorteile 229 Zend_Measure 25 Zend_Mime 226 Zend_Pdf 26, 364, 368, 369<br /> <br /> $properties 366 clipCircle() 382 clipEllipse() 382 clipPolygon() 382 clipRectangle() 382 draw() 375 drawCircle() 379 drawEllipse() 380 drawImage() 381 drawPolygon() 377 drawRectangle() 377 drawText() 371 eigene Seitengröße und -ausrichtung 365 eigener Font 370 Formen füllen 377 Formen zeichnen 375 Grafikstatus speichern 370, 377, 381 imageWithPath() 381 Koordinatensystem 369 load() 364 newPage() 365 parse() 364 render() 367 rotate() 380 save() 367 Schnittmasken 382<br /> <br /> Seitenbreite 369 Seiten-Font setzen 370 Seitenhöhe 369 setFont() 371 Zend_Pdf_Color 373 Zend_Pdf_Color_Cmyk 373 Zend_Pdf_Color_GrayScale 373 Zend_Pdf_Color_Html 373 Zend_Pdf_Color_Rgb 373 Zend_Pdf_Font 370 Zend_Pdf_Page 364, 369<br /> <br /> FILL_METHOD_EVEN_ODD 377 FILL_METHOD_NON_ZERO_WINDIN G 377 restoreGS() 371 saveGS 371 setLineDashingPattern() 376 setStyle() 374 SHAPE_DRAW_FILL 377 SHAPE_DRAW_FILL_AND_STROKE 377 SHAPE_DRAW_STROKE 377 SIZE_A4 365 SIZE_A4_LANDSCAPE 365 SIZE_LETTER 365 SIZE_LETTER_LANDSCAPE 365 Standard-Style 374 Zend_Pdf_Style 374 Zend_Registry<br /> <br /> getInstance() 415 Zend_Registry 69, 306, 414<br /> <br /> Implementierung 415 Zend_Rest 289, 291 Zend_Rest_Client 291, 295<br /> <br /> restPost() 293 Zend_Rest_Client_Result 296 Zend_Rest_Server 294 Zend_Search_Lucene 26, 200<br /> <br /> addDocument() 202 Aktualisierung eines Dokuments 216 Best Practices 208 Binary, Feldtyp 202 Boolesche Operatoren 205 create() 201 Feldtypen 202 find() 203, 220 Keyword, Feldtyp 202 MaxBufferedDocs 209 optimize() 203 Text, Feldtyp 202 UnIndexed, Feldtyp 202<br /> <br /> 455<br /> <br /> Register UnStored, Feldtyp 202 UTF-8 209 Zend_Search_Lucene_Document 210 Zend_Search_Lucene_Index_Term 206 Zend_Search_Lucene_Search_Query 203 Zend_Search_Lucene_Search_Query_MultiTer m 206 Zend_Search_Lucene_Search_Query_Phrase 207 Zend_Search_Lucene_Search_Query_Range 208 Zend_Search_Lucene_Search_Query_Wildcard 207 Zend_Search_Lucene_Search_QueryHit 201 Zend_Search_Lucene_Search_QueryParser 204 Zend_Server_Reflection 287 Zend_Service 298 Zend_Service_Akismet 292, 300 Zend_Service_Amazon 301 Zend_Service_Amazon_Query 305, 306 Zend_Service_Audioscrobbler 301 Zend_Service_Delicious 301 Zend_Service_Flickr 302, 312 Zend_Service_Gdata 298 Zend_Service_Gravatar 302 Zend_Service_Nirvanix 302 Zend_Service_RememberTheMilk 303 Zend_Service_Simpy 303 Zend_Service_SlideShare 303 Zend_Service_StrikeIron 303 Zend_Service_Technorati<br /> <br /> cosmos() 304 Zend_Service_Technorati 304 Zend_Service_Yahoo 304<br /> <br /> pageDataSearch() 304, 305 Zend_Translate 25, 349, 357<br /> <br /> _(), Funktion 350 unterstützte Eingabeformate 350 Verwendung von gettext()-Format 351 Zend_Validate 23, 178, 179<br /> <br /> 456<br /> <br /> isValid() 180, 192 setMessage() 189 Zend_Validate_EmailAddress 226 Zend_Validate_StringLength 191 Zend_View 22, 33, 40, 47<br /> <br /> assign() 40, 47 Hilfsfunktionen 49 HTML-E-Mails rendern 239 Sicherheit 50 Skriptdatei 48 Zend_View_Helper 49 Zend_View_Helper_Escape 41 Zend_View_Helper_FormatCurrency 49 Zend_XmlRpc 279 Zend_XmlRpc_Client 25, 288<br /> <br /> call() 288 Zend_XmlRpc_Server 280, 281<br /> <br /> DocBlock 286 Typzuordnung 287 Zend_XmlRpc_Server_Cache 287 Zend_XmlRpc_Value 286<br /> <br /> Typzuordnung 287 XMLRPC_TYPE_DATETIME 287 ZFiA_Author 403 ZFiA_Person 400 Zieltabelle 152 Zugriffskontrolle 155, 156, 167<br /> <br /> 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<br /> <br /> rollenbasierte 167 Zugriffssteuerung<br /> <br /> Übersicht 24<br /> <br /> Struts 2 im Einsatz<br /> <br /> Brown/Davis/Stanlick Struts 2 im Einsatz 478 Seiten ISBN 978-3-446-41575-1<br /> <br /> 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<br /> <br /> JBoss im Detail<br /> <br /> Jamae/Johnson JBoss im Einsatz Den JBoss Application Server konfigurieren 543 Seiten ISBN 978-3-446-41574-4<br /> <br /> 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.<br /> <br /> Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer<br /> <br /> Frischer Wind für Java.<br /> <br /> Walls Spring im Einsatz 676 Seiten. ISBN 978-3-446-41240-8<br /> <br /> 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<br /> <br /> Mehr PHP gibt’s nicht!<br /> <br /> Krause PHP 5 – Grundlagen und Profiwissen 1344 Seiten. Mit CD. ISBN 3-446-40334-5<br /> <br /> 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.<br /> <br /> Mehr Informationen zu diesem Buch und zu unserem Programm unter www.hanser.de/computer<br /> <br /> 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.<br /> <br /> 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<br /> <br /> 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<br /> <br /> (Web-)Programmierer und Entwickler mit Vorkenntnissen in PHP<br /> <br /> 9<br /> <br /> 783446 415768<br /> <br /> 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.<br /> <br /> 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<br /> <br /> </div> </div> </div> <div class="row hidden-xs"> <div class="col-md-12"> <h2></h2> <hr /> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-im-einsatz3569673f5a068cf5da35ca6cffe42c3b37853.html"> <img src="https://epdf.tips/img/300x300/zend-framework-im-einsatz_5aef7f51b7d7bc58574dee3e.jpg" alt="ZEND Framework im Einsatz" /> <h3 class="note-title">ZEND Framework im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-im-einsatz3569673f5a068cf5da35ca6cffe42c3b37853.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/beginning-zend-frameworkb012fa7ebc69e83e2e24ad4b4fb255c547645.html"> <img src="https://epdf.tips/img/300x300/beginning-zend-framework_5b81282db7d7bc9a0450d4d7.jpg" alt="Beginning Zend Framework" /> <h3 class="note-title">Beginning Zend Framework</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/beginning-zend-frameworkb012fa7ebc69e83e2e24ad4b4fb255c547645.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-in-action.html"> <img src="https://epdf.tips/img/300x300/zend-framework-in-action_5ad7e368b7d7bc825f15228b.jpg" alt="Zend Framework in Action" /> <h3 class="note-title">Zend Framework in Action</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-in-action.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-in-action6fda77ce8818fc464ebc2960632243aa75929.html"> <img src="https://epdf.tips/img/300x300/zend-framework-in-action_5b4afe44b7d7bc9844124621.jpg" alt="Zend framework in action" /> <h3 class="note-title">Zend framework in action</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-in-action6fda77ce8818fc464ebc2960632243aa75929.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-web-servicesa801987be591c73ec7c10472ea965ae196138.html"> <img src="https://epdf.tips/img/300x300/zend-framework-web-services_5ac83c59b7d7bc0873e66460.jpg" alt="Zend Framework Web Services" /> <h3 class="note-title">Zend Framework Web Services</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-web-servicesa801987be591c73ec7c10472ea965ae196138.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-web-services.html"> <img src="https://epdf.tips/img/300x300/zend-framework-web-services_5ac83c49b7d7bc0873e6645f.jpg" alt="Zend Framework Web Services" /> <h3 class="note-title">Zend Framework Web Services</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-web-services.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/beginning-zend-framework669a9bfb3e676c3f06468f4fc63313a17398.html"> <img src="https://epdf.tips/img/300x300/beginning-zend-framework_5b8128c2b7d7bc9b048db689.jpg" alt="Beginning Zend Framework" /> <h3 class="note-title">Beginning Zend Framework</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/beginning-zend-framework669a9bfb3e676c3f06468f4fc63313a17398.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-20-cookbooka24c2a26746adbca2c88a42a98c3502793195.html"> <img src="https://epdf.tips/img/300x300/zend-framework-20-cookbook_5ab59998b7d7bccd12c651ab.jpg" alt="Zend Framework 2.0 Cookbook" /> <h3 class="note-title">Zend Framework 2.0 Cookbook</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-20-cookbooka24c2a26746adbca2c88a42a98c3502793195.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-20-cookbook.html"> <img src="https://epdf.tips/img/300x300/zend-framework-20-cookbook_5ab59993b7d7bccd12c651aa.jpg" alt="Zend Framework 2.0 Cookbook" /> <h3 class="note-title">Zend Framework 2.0 Cookbook</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-20-cookbook.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/beginning-zend-framework.html"> <img src="https://epdf.tips/img/300x300/beginning-zend-framework_5b812810b7d7bc9a0450d4d6.jpg" alt="Beginning Zend Framework" /> <h3 class="note-title">Beginning Zend Framework</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/beginning-zend-framework.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-a-beginners-guide.html"> <img src="https://epdf.tips/img/300x300/zend-framework-a-beginners-guide_5ae41299b7d7bc47576e9b84.jpg" alt="Zend Framework, A Beginner's Guide" /> <h3 class="note-title">Zend Framework, A Beginner's Guide</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-a-beginners-guide.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-a-beginners-guide12f6528986ae395124d246607f7f0cb988894.html"> <img src="https://epdf.tips/img/300x300/zend-framework-a-beginners-guide_5ae412f9b7d7bc47576e9b87.jpg" alt="Zend Framework, A Beginner's Guide" /> <h3 class="note-title">Zend Framework, A Beginner's Guide</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-a-beginners-guide12f6528986ae395124d246607f7f0cb988894.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-a-beginners-guide-pdf-5eccd5a9d18b5.html"> <img src="https://epdf.tips/img/300x300/zend-framework-a-beginners-guide_5eccd5a9097c47770a8b52e0.jpg" alt="Zend Framework, A Beginner's Guide" /> <h3 class="note-title">Zend Framework, A Beginner's Guide</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-a-beginners-guide-pdf-5eccd5a9d18b5.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/zend-framework-a-beginners-guide-pdf-5eccd5a951fa3.html"> <img src="https://epdf.tips/img/300x300/zend-framework-a-beginners-guide_5eccd5a9097c47770a8b52df.jpg" alt="Zend Framework, A Beginner's Guide" /> <h3 class="note-title">Zend Framework, A Beginner's Guide</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/zend-framework-a-beginners-guide-pdf-5eccd5a951fa3.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/asse-im-einsatz2d0214b6e932b15e0dd0390b60937bc164276.html"> <img src="https://epdf.tips/img/300x300/asse-im-einsatz_5b9a1d7fb7d7bcc61c84bdbb.jpg" alt="Asse im Einsatz" /> <h3 class="note-title">Asse im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/asse-im-einsatz2d0214b6e932b15e0dd0390b60937bc164276.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/gwt-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/gwt-im-einsatz_5b9a1d6db7d7bcc81c7ab0ba.jpg" alt="GWT im Einsatz" /> <h3 class="note-title">GWT im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/gwt-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/mutanten-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/mutanten-im-einsatz_5b9a1d64b7d7bcca1c8fe300.jpg" alt="Mutanten im Einsatz" /> <h3 class="note-title">Mutanten im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/mutanten-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/roboter-im-einsatz2d0214b6e932b15e0dd0390b60937bc1202.html"> <img src="https://epdf.tips/img/300x300/roboter-im-einsatz_5b9a1d80b7d7bcc81c7ab0bb.jpg" alt="Roboter im Einsatz" /> <h3 class="note-title">Roboter im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/roboter-im-einsatz2d0214b6e932b15e0dd0390b60937bc1202.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/tsunamis-im-einsatzd5c7a6cdd055c135a2e6127d1fbe8dce93135.html"> <img src="https://epdf.tips/img/300x300/tsunamis-im-einsatz_5b9a1d76b7d7bcc61c84bdba.jpg" alt="Tsunamis im Einsatz" /> <h3 class="note-title">Tsunamis im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/tsunamis-im-einsatzd5c7a6cdd055c135a2e6127d1fbe8dce93135.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/tsunamis-im-einsatz7cc98eaeb140cc6b057e7559c8667b6283112.html"> <img src="https://epdf.tips/img/300x300/tsunamis-im-einsatz_5b9a1d6cb7d7bcca1c8fe301.jpg" alt="Tsunamis im Einsatz" /> <h3 class="note-title">Tsunamis im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/tsunamis-im-einsatz7cc98eaeb140cc6b057e7559c8667b6283112.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/mutanten-im-einsatzd848958b5233f9633957c16bc4aa39d477748.html"> <img src="https://epdf.tips/img/300x300/mutanten-im-einsatz_5b9a1d72b7d7bcca1c8fe302.jpg" alt="Mutanten im Einsatz" /> <h3 class="note-title">Mutanten im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/mutanten-im-einsatzd848958b5233f9633957c16bc4aa39d477748.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/androiden-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/androiden-im-einsatz_5b9a1d5db7d7bcca1c8fe2ff.jpg" alt="Androiden im Einsatz" /> <h3 class="note-title">Androiden im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/androiden-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/tsunamis-im-einsatz725140a57336c1a4772f7afefa9f54581632.html"> <img src="https://epdf.tips/img/300x300/tsunamis-im-einsatz_5b9a1d6bb7d7bcc61c84bdb8.jpg" alt="Tsunamis im Einsatz" /> <h3 class="note-title">Tsunamis im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/tsunamis-im-einsatz725140a57336c1a4772f7afefa9f54581632.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/tsunamis-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/tsunamis-im-einsatz_5b9a1d65b7d7bcc61c84bdb7.jpg" alt="Tsunamis im Einsatz" /> <h3 class="note-title">Tsunamis im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/tsunamis-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/asse-im-einsatz239786305b05c9ab8e7b3b68d0da5dba49299.html"> <img src="https://epdf.tips/img/300x300/asse-im-einsatz_5b9a1d7cb7d7bcca1c8fe304.jpg" alt="Asse im Einsatz" /> <h3 class="note-title">Asse im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/asse-im-einsatz239786305b05c9ab8e7b3b68d0da5dba49299.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/roboter-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/roboter-im-einsatz_5b9a1d5db7d7bcc61c84bdb6.jpg" alt="Roboter im Einsatz" /> <h3 class="note-title">Roboter im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/roboter-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/androiden-im-einsatzd848958b5233f9633957c16bc4aa39d44790.html"> <img src="https://epdf.tips/img/300x300/androiden-im-einsatz_5b9a1d72b7d7bcc61c84bdb9.jpg" alt="Androiden im Einsatz" /> <h3 class="note-title">Androiden im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/androiden-im-einsatzd848958b5233f9633957c16bc4aa39d44790.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/mutanten-im-einsatz1490e50cd7099bc233bcbbaf84c5041d40344.html"> <img src="https://epdf.tips/img/300x300/mutanten-im-einsatz_5b9a1d76b7d7bcca1c8fe303.jpg" alt="Mutanten im Einsatz" /> <h3 class="note-title">Mutanten im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/mutanten-im-einsatz1490e50cd7099bc233bcbbaf84c5041d40344.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/asse-im-einsatz.html"> <img src="https://epdf.tips/img/300x300/asse-im-einsatz_5b9a1d54b7d7bcc61c84bdb5.jpg" alt="Asse Im Einsatz" /> <h3 class="note-title">Asse Im Einsatz</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/asse-im-einsatz.html">Read more</a> </div> </div> </div> <div class="col-lg-2 col-md-3"> <div class="note"> <div class="note-meta-thumb"> <a href="https://epdf.tips/easy-php-websites-with-the-zend-framework71843.html"> <img src="https://epdf.tips/img/300x300/easy-php-websites-with-the-zend-framework_5a60e217b7d7bc18602f456e.jpg" alt="Easy PHP Websites with the Zend Framework" /> <h3 class="note-title">Easy PHP Websites with the Zend Framework</h3> </a> </div> <div class="note-action"> <a class="more-link" href="https://epdf.tips/easy-php-websites-with-the-zend-framework71843.html">Read more</a> </div> </div> </div> </div> </div> <div class="col-lg-3 col-md-4 col-xs-12"> <div class="panel-recommend panel panel-primary"> <div class="panel-heading"> <h4 class="panel-title">Recommend Documents</h4> </div> <div class="panel-body"> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-im-einsatz3569673f5a068cf5da35ca6cffe42c3b37853.html"> <img src="https://epdf.tips/img/60x80/zend-framework-im-einsatz_5aef7f51b7d7bc58574dee3e.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-im-einsatz3569673f5a068cf5da35ca6cffe42c3b37853.html"> ZEND Framework im Einsatz </a> </label> <div class="note-meta"> <div class="note-desc">FRAMEWORK IM EINSATZ ZEND FRAMEWORK IM EINSATZ rob ALLEN nick LO steven BROWN Allen, Lo, Brown Zend Framework im Ein...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/beginning-zend-frameworkb012fa7ebc69e83e2e24ad4b4fb255c547645.html"> <img src="https://epdf.tips/img/60x80/beginning-zend-framework_5b81282db7d7bc9a0450d4d7.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/beginning-zend-frameworkb012fa7ebc69e83e2e24ad4b4fb255c547645.html"> Beginning Zend Framework </a> </label> <div class="note-meta"> <div class="note-desc">CYAN MAGENTA YELLOW BLACK PANTONE 123 C BOOKS FOR PROFESSIONALS BY PROFESSIONALS ® THE EXPERT’S VOICE ® IN OPEN SOURC...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-in-action.html"> <img src="https://epdf.tips/img/60x80/zend-framework-in-action_5ad7e368b7d7bc825f15228b.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-in-action.html"> Zend Framework in Action </a> </label> <div class="note-meta"> <div class="note-desc">Zend Framework in Action Zend Framework in Action ROB ALLEN NICK LO STEVEN BROWN MANNING Greenwich (74° w. long.) ...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-in-action6fda77ce8818fc464ebc2960632243aa75929.html"> <img src="https://epdf.tips/img/60x80/zend-framework-in-action_5b4afe44b7d7bc9844124621.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-in-action6fda77ce8818fc464ebc2960632243aa75929.html"> Zend framework in action </a> </label> <div class="note-meta"> <div class="note-desc">Zend Framework in Action Zend Framework in Action ROB ALLEN NICK LO STEVEN BROWN MANNING Greenwich (74° w. long.) ...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-web-servicesa801987be591c73ec7c10472ea965ae196138.html"> <img src="https://epdf.tips/img/60x80/zend-framework-web-services_5ac83c59b7d7bc0873e66460.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-web-servicesa801987be591c73ec7c10472ea965ae196138.html"> Zend Framework Web Services </a> </label> <div class="note-meta"> <div class="note-desc"></div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-web-services.html"> <img src="https://epdf.tips/img/60x80/zend-framework-web-services_5ac83c49b7d7bc0873e6645f.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-web-services.html"> Zend Framework Web Services </a> </label> <div class="note-meta"> <div class="note-desc">Zend Framework Web Services A php|architect Guide by Jonas Mariën Zend Framework Web Services Contents Copyright ©2010...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/beginning-zend-framework669a9bfb3e676c3f06468f4fc63313a17398.html"> <img src="https://epdf.tips/img/60x80/beginning-zend-framework_5b8128c2b7d7bc9b048db689.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/beginning-zend-framework669a9bfb3e676c3f06468f4fc63313a17398.html"> Beginning Zend Framework </a> </label> <div class="note-meta"> <div class="note-desc">CYAN MAGENTA YELLOW BLACK PANTONE 123 C BOOKS FOR PROFESSIONALS BY PROFESSIONALS ® THE EXPERT’S VOICE ® IN OPEN SOURC...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-20-cookbooka24c2a26746adbca2c88a42a98c3502793195.html"> <img src="https://epdf.tips/img/60x80/zend-framework-20-cookbook_5ab59998b7d7bccd12c651ab.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-20-cookbooka24c2a26746adbca2c88a42a98c3502793195.html"> Zend Framework 2.0 Cookbook </a> </label> <div class="note-meta"> <div class="note-desc">Zend Framwork 2.0 Cookbook RAW Book Over 80 highly focused practical development recipes to maximize the Zend Framewo...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/zend-framework-20-cookbook.html"> <img src="https://epdf.tips/img/60x80/zend-framework-20-cookbook_5ab59993b7d7bccd12c651aa.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/zend-framework-20-cookbook.html"> Zend Framework 2.0 Cookbook </a> </label> <div class="note-meta"> <div class="note-desc">Zend Framwork 2.0 Cookbook RAW Book Over 80 highly focused practical development recipes to maximize the Zend Framewo...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> <div class="row m-0"> <div class="col-md-3 col-xs-3 pl-0 text-center"> <a href="https://epdf.tips/beginning-zend-framework.html"> <img src="https://epdf.tips/img/60x80/beginning-zend-framework_5b812810b7d7bc9a0450d4d6.jpg" alt="" width="100%" /> </a> </div> <div class="col-md-9 col-xs-9 p-0"> <label> <a href="https://epdf.tips/beginning-zend-framework.html"> Beginning Zend Framework </a> </label> <div class="note-meta"> <div class="note-desc">CYAN MAGENTA YELLOW BLACK PANTONE 123 C BOOKS FOR PROFESSIONALS BY PROFESSIONALS ® THE EXPERT’S VOICE ® IN OPEN SOURC...</div> </div> </div> <div class="clearfix"></div> <hr class="mt-15 mb-15" /> </div> </div> </div> </div> </div> </div> <div class="modal fade" id="report" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <form role="form" method="post" action="https://epdf.tips/report/zend-framework-im-einsatz" style="border: none;"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title">Report "Zend Framework im Einsatz"</h4> </div> <div class="modal-body"> <div class="form-group"> <label>Your name</label> <input type="text" name="name" required="required" class="form-control" /> </div> <div class="form-group"> <label>Email</label> <input type="email" name="email" required="required" class="form-control" /> </div> <div class="form-group"> <label>Reason</label> <select name="reason" required="required" class="form-control"> <option value="">-Select Reason-</option> <option value="pornographic" selected="selected">Pornographic</option> <option value="defamatory">Defamatory</option> <option value="illegal">Illegal/Unlawful</option> <option value="spam">Spam</option> <option value="others">Other Terms Of Service Violation</option> <option value="copyright">File a copyright complaint</option> </select> </div> <div class="form-group"> <label>Description</label> <textarea name="description" required="required" rows="3" class="form-control" style="border: 1px solid #cccccc;"></textarea> </div> <div class="form-group"> <div style="display: inline-block;"> <div class="g-recaptcha" data-sitekey="6Lemmz0UAAAAAANSnNH_YtG0406jaTUcUP7mxrLr"></div> </div> </div> <script src='https://www.google.com/recaptcha/api.js'></script> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="submit" class="btn btn-success">Send</button> </div> </form> </div> </div> </div> <footer class="footer" style="margin-top: 60px;"> <div class="container-fluid"> Copyright © 2024 EPDF.TIPS. All rights reserved. <div class="pull-right"> <span><a href="https://epdf.tips/about">About Us</a></span> | <span><a href="https://epdf.tips/privacy">Privacy Policy</a></span> | <span><a href="https://epdf.tips/term">Terms of Service</a></span> | <span><a href="https://epdf.tips/copyright">Copyright</a></span> | <span><a href="https://epdf.tips/dmca">DMCA</a></span> | <span><a href="https://epdf.tips/contact">Contact Us</a></span> | <span><a href="https://epdf.tips/cookie_policy">Cookie Policy</a></span> </div> </div> </footer> <!-- Modal --> <div class="modal fade" id="login" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close" on="tap:login.close"><span aria-hidden="true">×</span></button> <h4 class="modal-title" id="add-note-label">Sign In</h4> </div> <div class="modal-body"> <form action="https://epdf.tips/login" method="post"> <div class="form-group"> <label class="sr-only" for="email">Email</label> <input class="form-input form-control" type="text" name="email" id="email" value="" placeholder="Email" /> </div> <div class="form-group"> <label class="sr-only" for="password">Password</label> <input class="form-input form-control" type="password" name="password" id="password" value="" placeholder="Password" /> </div> <div class="form-group"> <div class="checkbox"> <label class="form-checkbox"> <input type="checkbox" name="remember" value="1" /> <i class="form-icon"></i> Remember me </label> <label class="pull-right"><a href="https://epdf.tips/forgot">Forgot password?</a></label> </div> </div> <button class="btn btn-primary btn-block" type="submit">Sign In</button> </form> <hr style="margin-top: 15px;" /> <a href="https://epdf.tips/login/facebook" class="btn btn-facebook btn-block"><i class="fa fa-facebook"></i> Login with Facebook</a> </div> </div> </div> </div> <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-111550345-1"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-111550345-1'); </script> <script src="https://epdf.tips/assets/js/jquery-ui.min.js"></script> <link rel="stylesheet" href="https://epdf.tips/assets/css/jquery-ui.css"> <script> $(function () { $("#document_search").autocomplete({ source: function (request, response) { $.ajax({ url: "https://epdf.tips/suggest", dataType: "json", data: { term: request.term }, success: function (data) { response(data); } }); }, autoFill: true, select: function( event, ui ) { $(this).val(ui.item.value); $(this).parents("form").submit(); } }); }); </script> <!-- cookie policy --> <div id="EPDFTIPS_cookie_box" style="z-index:99999; border-top: 1px solid #fefefe; background: #97c479; width: 100%; position: fixed; padding: 5px 15px; text-align: center; left:0; bottom: 0;"> Our partners will collect data and use cookies for ad personalization and measurement. <a href="https://epdf.tips/cookie_policy" target="_blank">Learn how we and our ad partner Google, collect and use data</a>. <a href="#" class="btn btn-success" onclick="accept_EPDFTIPS_cookie_box();return false;">Agree & close</a> </div> <script> function accept_EPDFTIPS_cookie_box() { document.cookie = "EPDFTIPS_cookie_box_viewed=1;max-age=15768000;path=/"; hide_EPDFTIPS_cookie_box(); } function hide_EPDFTIPS_cookie_box() { var cb = document.getElementById('EPDFTIPS_cookie_box'); if (cb) { cb.parentElement.removeChild(cb); } } (function () { var EPDFTIPS_cookie_box_viewed = (function (name) { var matches = document.cookie.match(new RegExp("(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)")); return matches ? decodeURIComponent(matches[1]) : undefined; })('EPDFTIPS_cookie_box_viewed'); if (EPDFTIPS_cookie_box_viewed) { hide_EPDFTIPS_cookie_box(); } })(); </script> <!-- end cookie policy --> </body> </html> <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>