Contact form

1. Introduction

The present tutorial provides another use case of both Forms and the <core:importdesign /> tag.

The contact form described within this chapter includes securing recipient addresses from grabbers and bots that gather e-mail addresses from web pages. The list of recipients can be configured and extended as needed. The module can be included in any application via the <core:importdesign /> tags at any position.

Formatting of the module's output is done by an appropriate CSS file. As a template you may want to use shipped CSS file /modules/contact/pres/css/contact.css from the release package. It must be added to the head of the corresponding page. This file contains a basic set of formatting for the module, that must be adapted as desired. A detailed description of the generated HTML code can be taken from the wiki chapter Kontakt-Formular-Modul (German).

2. Basics

This tutorial uses techniques that have been discussed in chapters

Thus we recommend that you have red them prior working with this one.

The subsequent sections describe the configuration of the module to prepare it for usage within different applications and with labels in different languages. Further, the structure and the construction of the software is discussed.

2.1. Configuration

To be able to integrate the contact form module in various projects the project specific parts must be outsourced to application configuration files. Configuration is necessary in two flavours: first of all the recipients must be configured, second the texts of the form tag libs (validators) must be defined per project.

2.2. Multi-language support

The Adventure PHP Framework features several options to create multi-language applications or modules. Basically, the language is held within every single DOM node or service layer. This property can be used to display language dependent texts using a document controller or XML tag <html:getstring />. For details, please see Standard taglibs. The contact form uses XML tags within different areas. The content is based within a common translation file:

APF template

The file itself contains the following values:

APF configuration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 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: " = "Ihr Name:" = "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 = "Bitte füllen Sie das Feld Absender-Name!" = "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: " = "Your name:" = "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 = "Please provide a sender name!" = "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!"

The above values can be used by the following tag examples:

APF template
<html:getstring namespace="APF\modules\contact" config="language.ini" entry="formhint.text" /> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="" />

Besides using XML tags to apply labels you may also use the following code within controllers:

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. Structure of the module

According to the structure described in the comment module tutorial (see Comment function) the structure of the contact module looks like this:

APF template
APF/modules/ contact/ biz/ data/ pres/ css/ controller/ templates/ mail/

To structure the software folders data, biz, and pres have been created. Within pres one folder for both controllers (controller) and view components (templates) are contained. Further, folder css contains a sample stylesheet.

Implementing custom modules it is not recommended to store them within the modules folder since this might break the update path. Since the contact module is part of the APF this is appropriate here.

Moreover, the following configuration files are used by the module:

  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_recipients.ini: Configuration of recipients
  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_language.ini: Language-dependent texts
  • /APF/config/modules/contact/{CONTEXT}/{ENVIRONMENT}_mail_templates.ini: Configuration of e-mail templates

3. Implementation of the module

This section describes the implementation step-by-step starting with the presentation layer.

3.1. File contact.html

Template file contact.html defines the skeleton of the module. It contains the headline displayed by a <html:getstring /> tag from the translation file and a <core:importdesign /> tag that displays the dynamic area: either the form (default) of the thanks page.

The template file is as follows:

APF template
<div class="contact-main"> <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>

Within the above template file the <core:importdesign /> tag is used with the pagepart option in effect. this means that depending on the URL parameter contactview a corresponding template from the given namespace is included. In case no URL parameter is given, the default template specified along with the template attribute is displayed - the form.

Details can be taken from chapter Standard taglibs.

3.2. File form.html

Template file form.html defines the input part of the contact form. It defines the (Document-)Controller used, the HTML skeleton and the form itself:

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"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="" /></li> </form:listener> <form:listener control="sender-address" id="addr-error"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="" /></li> </form:listener> <form:listener control="subject" id="subject-error"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="form.subject.error" /></li> </form:listener> <form:listener control="content" id="text-error"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="form.text.error" /></li> </form:listener> <form:listener control="captcha-control" id="captcha-error"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="form.captcha.error" /></li> </form:listener> <form:error id="bottomerror"> </ul> </div> </form:error> <label for="contact-form-recipient"> <html: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"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="" /> </label> <form:text id="contact-form-sendername" name="sender-name" /> <label for="contact-form-recipient-email"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="" /> </label> <form:text id="contact-form-recipient-email" name="sender-address" /> <label for="contact-form-subject"> <html: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"> <html: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"> <html:getstring namespace="APF\modules\contact" config="language.ini" entry="form.captcha" /> </label> <core: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" /> </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> <form:addfilter class="APF\tools\form\filter\EMailFilter" button="sendFormContact" control="sender-address" /> <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:addvalidator class="APF\modules\captcha\pres\validator\CaptchaValidator" control="captcha-control" button="sendFormContact" /> </div> </div> </html:form> </div>

Displaying the error messages, several listener tags are used. They refer to dedicated form controls and are activated on error. The message itself is taken from the translation file.

To protect the form against machine attacks the CAPTCHA tag (for forms) has been integrated.

3.3. File thanks.html

Within file thanks.html the thanks message is defined, that is displayed on submission of the form. Also in this case the message is taken from the translation file. This means, that no document controller is required:

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

3.4. Document controller ContactFormController

As mentioned in chapter 3.2 a document controller is required to manage the user input. Thus, the ContactFormController takes responsibility for filling the recipient list as well as handling an incoming contact request.

Chapter (Document-)Controller describes the implementation of document controllers. In general, a custom controller implements the DocumentController interface or derives from BaseDocumentController respectively. In this case the controller used the second option which is recommended anyway - ContactFormController inherits from BaseDocumentController.

Managing the use cases described above method transformContent() is implemented. It shows the form as long as the user input is valid. If true, it and picks up the form values and sends the form using the business component ContactManager:

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

Creation of the business component is done by the ServiceManager. This framework component is able to create objects and initializes them for usage within the APF context. Details can be taken from section Services.

ContactManager::sendContactForm() takes an instance of ContactFormData as an argument to send the form and display the thanks page.

3.5. Class ContactManager

Class ContactManager is the business component of the module. It encapsulates the business logic and talks to further interfaces or application layers (e.g. data layer, e-mail sending engine).

Essentially, the ContactManager offers two services: loadRecipients() to retrieve the list of recipients and sendContactForm() to send the contact form. ContactFormData and ContactFormRecipient have been introduced to abstract and encapsulate the form data as well as the recipient list.

The following code bpx contains the implementation of the component:

PHP code
namespace APF\modules\contact\biz; use APF\core\configuration\ConfigurationException; use APF\core\http\mixins\GetRequestResponse; 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\link\Url; use APF\tools\mail\MessageBuilder; use APF\tools\mail\Recipient; class ContactManager extends APFObject { use GetRequestResponse; public function sendContactForm(ContactFormData $formData) { $recipient = $this->getMapper()->loadRecipientById($formData->getRecipientId()); // send mail to notify the recipient $content = $this->getNotificationText( [ 'sender-name' => $formData->getSenderName(), 'sender-email' => $formData->getSenderEmail(), 'sender-subject' => $formData->getSubject(), 'sender-message' => $formData->getMessage(), 'recipient-name' => $recipient->getName(), 'recipient-email' => $recipient->getEmailAddress() ] ); /* @var $builder MessageBuilder */ $builder = $this->getServiceObject(MessageBuilder::class); $message = $builder->createMessage('ContactForm', $formData->getSubject(), $content); $message->addRecipient(new Recipient($recipient->getName(), $recipient->getEmailAddress())); $message->send(); // --------------------------------------------------------------------------------------------------------------- // send mail to notify the sender $content = $this->getConfirmationText( [ 'sender-name' => $formData->getSenderName(), 'sender-email' => $formData->getSenderEmail(), 'sender-subject' => $formData->getSubject(), 'sender-message' => $formData->getMessage(), 'recipient-name' => $recipient->getName(), 'recipient-email' => $recipient->getEmailAddress() ] ); $message = $builder->createMessage('ContactForm', $formData->getSubject(), $content); $message->addRecipient(new Recipient($formData->getSenderName(), $formData->getSenderEmail())); $message->send(); // redirect to the thanks page to avoid F5 bugs! $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(['contactview' => 'thanks'])); $this->getResponse()->forward($link); } private function &getMapper() { return $this->getServiceObject(ContactMapper::class); } private function getNotificationText(array $values = []) { $config = $this->getConfiguration('APF\modules\contact', 'mail_templates.ini'); if (!$config->hasSection($this->getLanguage())) { throw new ConfigurationException('Configuration section "' . $this->getLanguage() . '" is not present within ' . 'the contact form module configuration loading the email templates. Please ' . 'review your configuration!'); } $section = $config->getSection($this->getLanguage())->getSection('notification'); return $this->fillPlaceHolders( $this->getEmailTemplateContent( $section->getValue('namespace'), $section->getValue('template') ), $values ); } private function fillPlaceHolders($text, array $values = []) { 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!'); } private function getConfirmationText(array $values = []) { $config = $this->getConfiguration('APF\modules\contact', 'mail_templates.ini'); if (!$config->hasSection($this->getLanguage())) { throw new ConfigurationException('Configuration section "' . $this->getLanguage() . '" is not present within ' . 'the contact form module configuration loading the email templates. Please ' . 'review your configuration!'); } $section = $config->getSection($this->getLanguage())->getSection('confirmation'); return $this->fillPlaceHolders( $this->getEmailTemplateContent( $section->getValue('namespace'), $section->getValue('template') ), $values ); } public function loadRecipients() { return $this->getMapper()->loadRecipients(); } }

4. Class ContactMapper

Reading the recipient list has been encapsulated into a separate component within the contact module - the ContactMapper. It implements a data mapper that returns either a list of recipients or a single one via ContactFormRecipient.

Recipients are defined within configuration file


The file contains one section per recipient. The schema of one section is as follows:

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

The ContactMapper exposes two methods: loadRecipients() returns a list of all recipients and loadRecipientById() returns exactly one specific:

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

Main advantage of encapsulating loading recipients is that you only have to change the data layer implementation switching to another type of data source (e.g. database).


Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.