Kontaktformular-Tutorial

1. Einleitung

Das vorliegende Tutorial möchte eine weitere Anwendung der Formulare und des <core:importdesign />-Tags zeigen.

Das in diesem Tutorial beschriebene Kontakt-Formular bietet die Möglichkeit, Empfänger-Adressen vor Grabbern und Bots zu schützen oder die E-Mail-Adressen von Webseiten auslesen. Die Empfängerliste des Formulars ist beliebig konfigurier- und erweiterbar. Das Modul kann mit Hilfe des <core:importdesign />-Tags an beliebiger Stelle in eine bestehende Anwendung eingebunden werden.

Zur Formattierung der Ausgabe binden Sie bitte die zugehörige CSS-Datei ein. Als Vorlage können Sie die beiliegende CSS-Datei /modules/contact/pres/css/contact.css aus dem Release-Package nutzen. Diese beinhaltet bereits einen Basis-Satz an CSS-Definitionen und muss an den jeweiligen Stil der Seite angepasst werden. Eine genaue Beschreibung kann dem Wiki unter Kontakt-Formular-Modul entnommen werden.

2. Grundlagen

In diesem Tutorial werden Techniken eingesetzt, die in den Kapitel

beschrieben sind. Es wird daher davon aus gegangen, dass Sie die genannten Kapitel bereits gelesen und ggf. bearbeitet haben.

Die folgenden Kapitel gehen auf die Konfiguration des Moduls zum Einsatz in unterschiedlichen Anwendungen und in unterschiedlichen Sprachen sowie die Strutur bzw. den Aufbau der Software ein.

2.1. Konfiguration

Um die Applikation in unterschiedlichen Projekten einsetzbar zu machen müssen Projekt-spezifische Belange in Konfigurationsdateien ausgelagert werden. Konfiguration ist beim Kontaktformular in zweierlei Hinsicht notwendig: Zum einen müssen die Enpfänger-Namen und -Adressen konfigurierbar sein, zum anderen müssen die Ausgabetexte der verwendeten Formular-TagLibs (Validatoren) für das Projekt konfiguriert werden.

2.2. Mehrsprachigkeit

Das Framework bietet mehrere Möglichkeiten, Applikationen und Module auf Mehrsprachigkeit auszulegen. Grundsätzlich wird die Sprache eines Objekts in jedem DOM-Knoten mitgeführt. Der Entwickler hat damit die Möglichkeit sprachabhängige Texte in einem Document-Controller einzusetzen oder XML-Tags wie beispielsweise

  • <html:getstring />
  • <template:getstring />
  • <form:getstring />

zu nutzen um sprachabhängige Texte aus Konfigurationsdateien an enstprechender Stelle anzuzeigen (siehe Standard TagLibs). Das Kontaktformulars nutzt XML-Tags in verschiedenen Bereichen. Als Basis Übersetzungsdatei dient eine gemeinsame Konfigurationsdatei

APF-Template
/APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_language.ini

die mit folgenden Werten gefüllt ist:

APF-Konfiguration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Deutsche Texte ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [de] ; Header header.title = "Kontakt" ; Hinweise zum Formular formhint.text = "Wenn Sie mit dem APF-Entwickler-Team in Kontakt treten möchten, dann nutzen Sie [..]" ; Formular form.person = "Person / Gruppe: " form.name = "Ihr Name:" form.email = "Ihre E-Mail-Adresse:" form.subject = "Ihr Betreff:" form.comment = "Ihre Nachricht:" form.button = "Senden" form.captcha = "Bestätigungscode:" ; confirmation text message.text = "Vielen Danke für Ihre Anfrage. Wir werden uns umgehend mit Ihnen in Verbindung setzen!" ; validation messages form.name.error = "Bitte füllen Sie das Feld Absender-Name!" form.email.error = "Bitte geben Sie eine gültige E-Mail-Adresse sein!" form.subject.error = "Bitte füllen Sie das Feld Betreff!" form.text.error = "Bitte füllen Sie das Feld Text!" form.captcha.error = "Bitte füllen Sie das Captcha aus!" ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; English texts ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [en] ; header header.title = "Contact" ; hints on the form formhint.text = "If you want to contact the APF developer team, please use the form provided below. Then [..]" ; form labels form.person = "Person / group: " form.name = "Your name:" form.email = "Your email address:" form.subject = "Your subject:" form.comment = "Your message:" form.button = "Send" form.captcha = "Security code:" ; confirmation text message.text = "Many thanks for your message. We will get in contact with you immediately!" ; validation messages form.name.error = "Please provide a sender name!" form.email.error = "Please provide a valid email address!" form.subject.error = "Please fill the subject field!" form.text.error = "The message may not be empty!" form.captcha.error = "Please fill the captcha field!"

Die Werte der Datei können beispielsweise mit den folgenden Tag-Notationen ausgelesen werden:

APF-Template
<html:getstring namespace="APF\modules\contact" config="language.ini" entry="formhint.text" /> <listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.name.error" /> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.person" /> <button:getstring namespace="APF\modules\contact" config="language.ini" entry="form.button" />
Neben der Verwendung von XML-Tags lassen sich Beschriftungen auch innerhalb des Controllers realisieren. Beispiel:
PHP-Code
$form = & $this->getForm('contact'); $config = $this->getConfiguration('APF\modules\contact', 'language.ini'); $button = & $form->getFormElementByName('sendFormContact'); $label = $config->getSection($this->language)->getValue('form.button'); $button->setValue($label);

2.3. Struktur des Moduls

Ähnlich der Struktur des Moduls comments (siehe Kommentar-Funktion) liegt diese Software im Ordner modules ab. Hierzu wurde der Ordner contact angelegt, der wie folgt strukturiert ist:

APF-Template
APF/modules/ contact/ biz/ data/ pres/ css/ controller/ templates/ mail/

Zur Strukturierung der Software wurden die Ordner data, biz und pres angelegt. Innerhalb von pres existieren je ein Ordner für Controller (controller) und View-Komponenten (templates). Der Ordner css liefert Stylesheets mit.

Für die Implementierung von eigenen Modulen wird davon abgeraten, den Ordner modules zu nutzen, da sonst eigene Software beim Update u.U. überschrieben wird. Das das Kontakt-Modul ein Teil des APF ist, ist diese Vorgehensweise für dieses Tutorial in Ordnung.

Darüber hinaus werden folgende folgende Konfigurations-Dateien vorgehalten:

  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_recipients.ini: Konfiguration der Empfänger
  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_language.ini: Sprachabhängige Texte
  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_mail_templates.ini: Konfiguration der E-Mail-Templates

3. Implementierung des Moduls

Das vorliegende Kapitel beschreibt Schritt für Schritt die Implementierung des Moduls ausgehend von der Präsentations-Schicht.

3.1. Datei contact.html

Die Templatedatei contact.html definiert das Grundgerüst des Moduls. Sie beinhaltet die Überschrift, die per <html:getstring />-Tag aus der Sprach-Konfigurationsdatei ausgelesen wird und einen <core:importdesign />-Tag, der den dynamischen Bereich definiert: entweder das Formular (Standard) oder eine Danke-Seite.

Die Template-Datei hat folgenden Inhalt:

APF-Template
<div class="contact-main"> <h2><html:getstring namespace="APF\modules\contact" config="language.ini" entry="header.title" /></h2> <core:importdesign namespace="APF\modules\contact\pres\templates" incparam="contactview" template="[contactview=form]" /> </div>

Im Template wird der <core:importdesign />-Tag mit der pagepart-Option benutzt. Dies bedeutet, dass je nach URL-Parameter contactview ein anderes Template aus den angegebenen Namespace eingebunden wird. Ist kein Parameter in der URL vorhanden wird die im Attribut template angegebene Templatedatei - das Formular - eingebunden.

Details entnehmen Sie bitte dem Kapitel Standard TagLibs.

3.2. Datei form.html

Die Template-Datei form.html definiert die Eingabe-Maske des Kontakt-Formulars. Sie referenziert den zuständigen (Document-)Controller, definiert das HTML-Gerüst sowie das Formular selbst:

APF-Template
<@controller class="APF\modules\contact\pres\controller\ContactFormController" @> <core:addtaglib class="APF\tools\form\taglib\HtmlFormTag" prefix="html" name="form" /> <p> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="formhint.text" /> </p> <div class="contact-form"> <html:form name="contact" method="post"> <div> <form:error id="toperror"> <div class="error-container"> <ul> </form:error> <form:listener control="sender-name" id="sender-error"> <li><listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.name.error" /></li> </form:listener> <form:listener control="sender-address" id="addr-error"> <li><listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.email.error" /></li> </form:listener> <form:listener control="subject" id="subject-error"> <li><listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.subject.error" /></li> </form:listener> <form:listener control="content" id="text-error"> <li><listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.text.error" /></li> </form:listener> <form:listener control="captcha-control" id="captcha-error"> <li><listener:getstring namespace="APF\modules\contact" config="language.ini" entry="form.captcha.error" /></li> </form:listener> <form:error id="bottomerror"> </ul> </div> </form:error> <form:addvalidator class="APF\tools\form\validator\TextLengthValidator" button="sendFormContact" control="sender-name|subject|content" /> <form:addvalidator class="APF\tools\form\validator\EMailValidator" button="sendFormContact" control="sender-address" /> <form:addfilter class="APF\tools\form\filter\EMailFilter" button="sendFormContact" control="sender-address" /> <label for="contact-form-recipient"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.person" /> </label> <form:select id="contact-form-recipient" name="recipient" /> <label for="contact-form-sendername"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.name" /> </label> <form:text id="contact-form-sendername" name="sender-name" /> <label for="contact-form-recipient-email"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.email" /> </label> <form:text id="contact-form-recipient-email" name="sender-address" /> <label for="contact-form-subject"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.subject" /> </label> <form:text id="contact-form-subject" name="subject" /> <label for="contact-form-textarea"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.comment" /> </label> <form:area id="contact-form-textarea" name="content" cols="50" rows="6"/> <div class="fullsizebox captchabox"> <label for="contact-form-captcha"> <form:getstring namespace="APF\modules\contact" config="language.ini" entry="form.captcha" /> </label> <form:addtaglib class="APF\modules\captcha\pres\taglib\SimpleCaptchaTag" prefix="form" name="captcha" /> <form:captcha id="contact-form-captcha" name="captcha-control" clearonerror="true" text_id="contact-form-captcha" disable_inline="true" /> <form:addvalidator class="APF\modules\captcha\pres\validator\CaptchaValidator" control="captcha-control" button="sendFormContact" /> </div> <div class="fullsizebox buttonbox"> <form:button name="sendFormContact" class="button"> <button:getstring namespace="APF\modules\contact" config="language.ini" entry="form.button" /> </form:button> </div> </div> </html:form> </div>

Zur Ausgabe der Fehlermeldungen werden mehrere Listener-Tags verwendet, die bei Fehlern der referenzierten Formular-Elemente aktiviert werden. Die Inhalte der Fehlermeldungen werden aus der gemeinsamen Sprachdatei ausgelesen.

Um das Formular vor maschinellen Angriffen zu schützen kommt die CAPTCHA-Taglib (für Formulare) zum Einsatz.

3.3. Datei thanks.html

In der Datei thanks.html wird die Dankesmeldung definiert, die bei erfolgreichem Absenden des Formulars eingeblendet wird. Auch diese wird in der gemeinsamen Übersetzungsdatei definiert. Zur Ausgabe ist daher kein Controller erforderlich:

APF-Template
<p> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="message.text" /> </p>

3.4. Document-Controller ContactFormController

Zur Steuerung der Eingabe des Formulars ist, wie in Kapitel 3.2 angesprochen, ein Controller erforderlich. Der ContactFormController kümmerst sich um das Füllen der Auswahlliste von Empfängern und die Entgegennahme der Kontakt-Anfrage.

Wie im Kapitel (Document-)Controller beschrieben implementiert ein Document-Controller das DocumentController-Interface bzw. leitet von BaseDocumentController ab. In diesem Fall - und dies ist die empfohlene Vorgehensweise - leitet der ContactFormController von BaseDocumentController ab.

Zu Steuerung implementiert die Klasse die Methode transformContent(). Sie zeigt das Formular so lange an, bis alle als Pflichtfelder gekennzeichneten Eingabe-Möglichkeiten mit validen Werten ausgefüllt sind. Wurde das Formular korrekt ausgefüllt, extrahiert der Controller die Inhalte und schickt das Formular über die Business-Komponente ContactManager ab:

PHP-Code
namespace APF\modules\contact\pres\controller; use APF\core\pagecontroller\BaseDocumentController; use APF\modules\contact\biz\ContactFormData; use APF\modules\contact\biz\ContactManager; use APF\tools\form\taglib\SelectBoxTag; class ContactFormController extends BaseDocumentController { public function transformContent() { $form = & $this->getForm('contact'); // fill recipient list /* @var $recipients SelectBoxTag */ $recipients = & $form->getFormElementByName('recipient'); /* @var $cM ContactManager */ $cM = & $this->getServiceObject('APF\modules\contact\biz\ContactManager'); $recipientList = $cM->loadRecipients(); for ($i = 0; $i < count($recipientList); $i++) { $recipients->addOption($recipientList[$i]->getName(), $recipientList[$i]->getId()); } if ($form->isSent() && $form->isValid()) { $formData = new ContactFormData(); $option = & $recipients->getSelectedOption(); $recipientId = $option->getValue(); $formData->setRecipientId($recipientId); $name = & $form->getFormElementByName('sender-name'); $formData->setSenderName($name->getAttribute('value')); $email = & $form->getFormElementByName('sender-address'); $formData->setSenderEmail($email->getAttribute('value')); $subject = & $form->getFormElementByName('subject'); $formData->setSubject($subject->getAttribute('value')); $text = & $form->getFormElementByName('content'); $formData->setMessage($text->getContent()); /* @var $cM ContactManager */ $cM = & $this->getServiceObject('APF\modules\contact\biz\ContactManager'); $cM->sendContactForm($formData); } else { $form->transformOnPlace(); } } }

Die Erzeugung der Business-Komponente wird über den ServiceManager abgewickelt. Dieser Framework-Bestandteil kümmerst sich sowohl um die Erzeugung des Objekts als auch um die (einfache) Initialisierung zur Verwendung derselben im APF-Kontext. Details entnehmen Sie bitte dem Kapitel Services.

Die Methode ContactManager::sendContactForm() nimmt eine Instanz der Klasse ContactFormData entgegen und wickelt Versand sowie Anzeige der Danke-Seite ab.

3.5. Klasse ContactManager

Die Klasse ContactManager ist die Business-Komponente des Moduls. Sie kapselt die eigentliche Geschäfts-Logik und kommuniziert mit weiteren Schnittstellen bzw. Schichten (z.B. Daten-Schicht, E-Mail-Versand-Schnittstelle).

Im Wesentlichen stellt der ContactManager zwei Dienste zur Verfügung: loadRecipients() um Empfänger zu laden und sendContactForm() um das Formular abzusenden. ContactFormData und ContactFormRecipient dienen zur Abstraktion und Kapselung der Formular-Daten sowie dier Empfänger.

Die folgende Code-Box beihaltet die Implementierung der Komponente:

PHP-Code
namespace APF\modules\contact\biz; use APF\core\configuration\ConfigurationException; use APF\core\loader\RootClassLoader; use APF\core\pagecontroller\APFObject; use APF\core\pagecontroller\IncludeException; use APF\modules\contact\data\ContactMapper; use APF\tools\link\LinkGenerator; use APF\tools\http\HeaderManager; use APF\tools\link\Url; use APF\tools\mail\mailSender; class ContactManager extends APFObject { public function sendContactForm(ContactFormData $formData) { // set up the mail sender $mail = & $this->getAndInitServiceObject('APF\tools\mail\mailSender', 'ContactForm'); /* @var $mail mailSender */ $recipient = $this->getMapper()->loadRecipientById($formData->getRecipientId()); /* @var $recipient ContactFormRecipient */ $mail->setRecipient($recipient->getEmailAddress(), $recipient->getName()); $mail->setContent( $this->getNotificationText( array( 'sender-name' => $formData->getSenderName(), 'sender-email' => $formData->getSenderEmail(), 'sender-subject' => $formData->getSubject(), 'sender-message' => $formData->getMessage(), 'recipient-name' => $recipient->getName(), 'recipient-email' => $recipient->getEmailAddress() ) ) ); $mail->setSubject($formData->getSubject()); // send mail to notify the recipient $mail->sendMail(); $mail->clearRecipients(); $mail->clearCCRecipients(); $mail->clearContent(); $mail->setRecipient($formData->getSenderEmail(), $formData->getSenderName()); $mail->setContent( $this->getConfirmationText( array( 'sender-name' => $formData->getSenderName(), 'sender-email' => $formData->getSenderEmail(), 'sender-subject' => $formData->getSubject(), 'sender-message' => $formData->getMessage(), 'recipient-name' => $recipient->getName(), 'recipient-email' => $recipient->getEmailAddress() ) ) ); $mail->setSubject($formData->getSubject()); // send mail to notify the sender $mail->sendMail(); // redirect to the thanks page to avoid F5 bugs! $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array('contactview' => 'thanks'))); HeaderManager::forward($link); } public function loadRecipients() { return $this->getMapper()->loadRecipients(); } private function &getMapper() { return $this->getServiceObject('APF\modules\contact\data\ContactMapper'); } private function getNotificationText(array $values = array()) { $config = $this->getConfiguration('APF\modules\contact', 'mail_templates.ini'); $section = $config->getSection($this->getLanguage()); if ($section === null) { throw new ConfigurationException('Configuration section "' . $this->getLanguage() . '" is not present within ' . 'the contact form module configuration loading the email templates. Please ' . 'review your configuration!'); } return $this->fillPlaceHolders( $this->getEmailTemplateContent( $section->getValue('notification.namespace'), $section->getValue('notification.template') ), $values ); } private function getConfirmationText(array $values = array()) { $config = $this->getConfiguration('APF\modules\contact', 'mail_templates.ini'); $section = $config->getSection($this->getLanguage()); if ($section === null) { throw new ConfigurationException('Configuration section "' . $this->getLanguage() . '" is not present within ' . 'the contact form module configuration loading the email templates. Please ' . 'review your configuration!'); } return $this->fillPlaceHolders( $this->getEmailTemplateContent( $section->getValue('confirmation.namespace'), $section->getValue('confirmation.template') ), $values ); } private function fillPlaceHolders($text, array $values = array()) { foreach ($values as $key => $value) { $text = str_replace('{' . $key . '}', $value, $text); } return $text; } private function getEmailTemplateContent($namespace, $template) { $loader = RootClassLoader::getLoaderByNamespace($namespace); $rootPath = $loader->getRootPath(); $vendor = $loader->getVendorName(); $fqNamespace = str_replace('\\', '/', str_replace($vendor . '\\', '', $namespace)); $file = $rootPath . '/' . $fqNamespace . '/' . $template . '.html'; if (file_exists($file)) { return file_get_contents($file); } throw new IncludeException('Email template file "' . $file . '" cannot be loaded. ' . 'Please review your contact module configuration!'); } }

4. Klasse ContactMapper

Das Auslesen der Empfänger-Liste wurde im contact-Modul in eine eigene Komponente ausgelagert - dem ContactMapper. Er implementiert einen Data-Mapper, der die Liste der Empfänger - oder auch einen einzelnen - über das Domänen-Objekt ContactFormRecipient bereitstellt.

Die Empfänger selbst werden in der Konfigurations-Datei

Code
/APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_recipients.ini

definiert. Die Datei enthält je eine Sektion pro Empfänger. Das Schema einer Sektion lautet wie folgt:

APF-Konfiguration
[Contact ([0-9]+)] recipient-name = "(.*)" recipient-address = "(.*)"

Der ContactMapper stellt zwei Services bereit: loadRecipients() gibt die Liste aller Empfänger zurück und loadRecipientById() um einen Empfänger zu laden:

PHP-Code
namespace APF\modules\contact\data; use APF\core\pagecontroller\APFObject; use APF\modules\contact\biz\ContactFormRecipient; class ContactMapper extends APFObject { public function loadRecipients() { $config = $this->getConfiguration('APF\modules\contact', 'recipients.ini'); /* @var $recipients ContactFormRecipient[] */ $recipients = array(); foreach ($config->getSectionNames() as $name) { $section = $config->getSection($name); $count = count($recipients); preg_match('/Contact ([0-9]+)/i', $name, $matches); if(isset($matches[1])){ $recipients[$count] = new ContactFormRecipient(); $recipients[$count]->setId($matches[1]); $recipients[$count]->setName($section->getValue('recipient-name')); $recipients[$count]->setEmailAddress($section->getValue('recipient-address')); } } return $recipients; } public function loadRecipientById($id) { /* @var $recipients ContactFormRecipient[] */ $recipients = $this->loadRecipients(); if (!is_array($recipients)) { return null; } for ($i = 0; $i < count($recipients); $i++) { if ($recipients[$i]->getId() == $id) { return $recipients[$i]; } } return null; } }

Vorteil der Kapselung des Ladens der Empfänger ist, dass bei Nutzung einer Empfänger-Datenbank lediglich die Implementierungen der Daten-Schicht-Methoden ausgetauscht werden muss.

Kommentare

Möchten Sie den Artikel eine Anmerkung hinzufügen, oder haben Sie ergänzende Hinweise? Dann können Sie diese hier einfügen. Die bereits verfassten Anmerkungen und Kommentare finden Sie in der untenstehenden Liste.
« 1   »
Einträge/Seite: | 5 | 10 | 15 | 20 |
1
Christian 12.10.2008, 23:12:17
Hallo Reiner,

aktuell wird keine Prüfung unternommen, von welchem Server das Formular abgeschickt wird. Lediglich ein Bild-CAPTCHA schützt das Formular, was letztlich einen ähnlichen Effekt hat.

Wie würdest du eine derartige Prüfung implementieren? Wollen wir im Forum weiter diskutieren?

Viele Grüße,
Christian
2
Reiner Rottmann 18.08.2008, 14:32:39
Unterstützt das Formular auch eine Verifizierung, dass Anfragen nur vom Webserver und keinen anderen Seiten kommen dürfen? Bei der Auswahl eines Webformulars ist es imho besonders wichtig, dass dieses nicht für Spam verwendet werden kann...