Comment function

1. Introduction

This tutorial describes the technical construction of the APF's comment module. You will get in touch with the components of the functionality and you can try to write real life comments at the end of the page.

Further, you are introduced to some paradigms and design patterns that are described under Basics and they are connected to the application cases they are suitable for.

2. Implementation of the software

From the user's point of view the application has two parts: the list of comments and the form to add a comment. From the developer the functionality consists of the presentation components to display and add comments, a database including the logic to store and read entries.

These components are described within the subsequent chapters. The software itself is structure regarding the three layer architecture paradigm.

Since there are several approaches on software development such as top-down or bottom-up, we have to decide which way to go. For this tutorial we're choosing bottom-up.

2.1. Folder structure of the module

First of all, we are going to create the folder structure (a.k.a. known as namespaces). Since the comment function is a piece of software that can be used within various website projects (see Basics) the module resides in /APF/modules.

Please note, that the modules folder is reserved for parts of the APF. Adding new folders may result in loss of files updating your APF installation. Since the comment module is part of the APF the procedure is valid for this tutorial.

The module's name is comments that is the name of the main folder at the same time. To create a suitable structure, the sub folders data, biz und pres are created. Within the presdirectory we create one folder for the document controllers (controller) and one for the view templates (templates). The complete structure is as follows:

/apps /config /core /extensions /modules /comments /biz /data /pres /css /controller /templates /tools

Since some components require configuration files the folder structure contains an additional folder (config). Besides, the css folder is intended for the stylesheets shipped with the module.

2.2. Domain object

First of all, we create the domain object ArticleComment that is used by all tiers. The class resides within the ArticleComment.php file under /APF/modules/comments/biz and contains the following content:

PHP code
namespace APF\modules\comments\biz class ArticleComment { private $id = null; private $name; private $email; private $comment; private $date; private $time; private $categoryKey; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } public function getEmail() { return $this->email; } public function setEmail($email) { $this->email = $email; } public function getComment() { return $this->comment; } public function setComment($comment) { $this->comment = $comment; } public function getDate() { return $this->date; } public function setDate($date) { $this->date = $date; } public function getTime() { return $this->time; } public function setTime($time) { $this->time = $time; } public function getCategoryKey() { return $this->categoryKey; } public function setCategoryKey($categoryKey) { $this->categoryKey = $categoryKey; } }

2.3. Data layer

The data layer is represented by a dedicated DataMapper that is - for now - limited to read entries only.

To understand the further code, it is important to know that the pageable entry view is realized by the Pager. This component expects the data layer component to include a method to load a particular data object by it's unique id. For this reason, the mapper features the loadArticleCommentByID() function.

For now, we create the file commentMapper.php located under /APF/modules/comments/data with the content displayed within the next code box:

PHP code
namespace APF\modules\comments\data; use APF\core\database\AbstractDatabaseHandler; use APF\core\database\ConnectionManager; use APF\core\pagecontroller\APFObject; use APF\modules\comments\biz\ArticleComment; class ArticleCommentMapper extends APFObject { public function loadArticleCommentByID($commentId) { $SQL = $this->getConnection(); $select = 'SELECT ArticleCommentID, Name, EMail, Comment, Date, Time FROM article_comments WHERE ArticleCommentID = \'' . $commentId . '\';'; $result = $SQL->executeTextStatement($select); return $this->mapArticleComment2DomainObject($SQL->fetchData($result)); } public function saveArticleComment(ArticleComment $comment) { $conn = $this->getConnection(); if ($comment->getId() == null) { $insert = 'INSERT INTO article_comments (Name, EMail, Comment, Date, Time, CategoryKey) VALUES (\'' . $conn->escapeValue($comment->getName()) . '\',\'' . $conn->escapeValue($comment->getEmail()) . '\',\'' . $conn->escapeValue($comment->getComment()) . '\',CURDATE(),CURTIME(),\'' . $comment->getCategoryKey() . '\');'; $conn->executeTextStatement($insert); } } private function getConnection() { /* @var $cM ConnectionManager */ $cM = $this->getServiceObject('APF\core\database\ConnectionManager'); $config = $this->getConfiguration('APF\modules\comments', 'comments.ini'); $connectionKey = $config->getSection('Default')->getValue('Database.ConnectionKey'); if ($connectionKey == null) { throw new \InvalidArgumentException('[ArticleCommentMapper::getConnection()] The module\'s ' . 'configuration file does not contain a valid database connection key. Please ' . 'specify the database configuration according to the example configuration files!', E_USER_ERROR); } return $cM->getConnection($connectionKey); } private function mapArticleComment2DomainObject($resultSet) { $comment = new ArticleComment(); $comment->setId($resultSet['ArticleCommentID']); $comment->setName($resultSet['Name']); $comment->setEmail($resultSet['EMail']); $comment->setComment($resultSet['Comment']); $comment->setDate($resultSet['Date']); $comment->setTime($resultSet['Time']); return $comment; } }

The source code can be interpreted as follows:

  • Via several use statements dependent classes are declared.
  • loadArticleCommentByID() loads one single comment object from the database using the applied identifier. With this implementation, we avoid configurable field names or database tables. This makes additional mapping mechanisms obsolete and increases the performance. The database connection is retrieved by the ConnectionManager and the result array is applied to the mapArticleComment2DomainObject() method.
  • The mapArticleComment2DomainObject() is aimed to converted the result array to the domain object structure.
  • The getConnection() function returns the database connection configured for the module.

In order to setup the database layout please use the init_comments.sql script located in /APF/modules/comments/data/scripts.

For custom applications, we recommend to create setup and initializer scripts as well. If they are placed within the module's folder you are able to re-engineer the table usage after a while.

The configuration file {ENVIRONMENT}_comments.ini defines the database connection. The connection definition itself is contained in the APF\core\database namespace. Details can be taken from the ConnectionManager chapter.

2.4. Business layer

The business layer is represented by our model class a manager. Therefore, we create a file called commentManager.php under /APF/modules/comments/biz. Having a look at the comment function (read-only part) the business component only has to retrieve the desired data from the data layer and pass the result to the presentation layer.

As mentioned above, the comment module uses the pager. For this reason, further mechanisms must be implemented along with a configuration for the pager.

The basic structure is as follows:

PHP code
namespace APF\modules\comments\biz; class ArticleCommentManager extends APFObject { protected $categoryKey; public function init($initParam) { $this->categoryKey = $initParam; } public function loadEntries() { } }

Please note the subsequently listed hints:

  • The internal member variable $categoryKey stores the category that is relevant to handle comments.
  • To be able to dynamically pass the category to the business component using the service manager's getAndInitServiceObject() method the init() method must be implemented.
  • loadEntries() is a prototype of the implementation of the method that enables the presentation layer to load a set of entries.

Let us now focus on the integration of the Pager. This component will ve created and initialized using the DIServiceManager. For this reason, please create configuration file APF\config\modules\comments\{CONTEXT}\{ENVIRONMENT}_serviceobjects.ini and fill with the following lines:

PHP code
$pMF = $this->getServiceObject('APF\modules\pager\biz\PagerManagerFabric'); $pM = $pMF->getPagerManager('ArticleComments');

The configuration of the pager is located under the component's namespace (APF\modules\pager) and the current context of the application. There, the DEFAULT_pager.ini contains the following content:

APF configuration
[CommentsPager] servicetype = "SINGLETON" class = "APF\modules\pager\biz\PagerManager" conf.entries-per-page.method = "setEntriesPerPage" conf.entries-per-page.value = "10" conf.url-param-page.method = "setPageUrlParameterName" conf.url-param-page.value = "PgrPg" conf.url-param-count.method = "setCountUrlParameterName" conf.url-param-count.value = "PgrAnz" conf.statement-namespace.method = "setStatementNamespace" conf.statement-namespace.value = "APF\modules\comments" conf.count-statement.method = "setCountStatementFile" conf.count-statement.value = "load_entries_count.sql" conf.entries-statement.method = "setEntriesStatementFile" conf.entries-statement.value = "load_entry_ids.sql" conf.statement-params.method = "setStatementParameters" conf.statement-params.value = "CategoryKey:standard" conf.ui-namespace.method = "setPagerUiNamespace" conf.ui-namespace.value = "APF\modules\pager\pres\templates" conf.ui-template.method = "setPagerUiTemplate" conf.ui-template.value = "pager_2" conf.database-connection.method = "setDatabaseConnectionName" conf.database-connection.value = "MySQLi" conf.dynamic-page-size.method = "setAllowDynamicEntriesPerPage" conf.dynamic-page-size.value = "true" conf.caching.method = "setCacheInSession" conf.caching.value = "true"

Details on the parameters used can be found in chapter Pager.

To create and use the pager the following lines of code can be used:

PHP code
$pM = $this->getDIServiceObject('APF\modules\comments', 'CommentsPager');

After having configured the pager the implementation of the loadEntries() method is as follows:

PHP code
public function loadEntries() { $pager = $this->getPagerManager(); $m = $this->getServiceObject('APF\modules\comments\data\ArticleCommentMapper'); return $pager->loadEntriesByAppDataComponent($m, 'loadArticleCommentByID', array('CategoryKey' => $this->categoryKey)); }

First of all, the desired pager instance is created. Second, the application's data mapper is created and passed to the pager to directly load the real entries from the database.

In contrast to this approach the the pager can also be used to load the entry ids only. The list of ids can then be used to load the entry domain objects with the data mapper manually:

PHP code
public function loadEntries() { $pager = $this->getPagerManager(); $entries = $pager->loadEntries(array('CategoryKey' => $this->categoryKey)); $m = $this->getServiceObject('APF\modules\comments\data\ArticleCommentMapper'); $entryList = array(); for($i = 0; $i < count($entries); $i++){ $entryList[] = $m->loadArticleCommentByID($entries[$i]); } return $entryList; }

4.5. Presentation layer

The presentation layer is represented by only one view concerning the read only case: the list of comments. Let us now concentrate on the inclusion of the comment function using the <core:importdesign /> tag.

Beside the easy inclusion, the template developer must be given the possibility to define the category that limits the list of comments. Therefore, the categorykey is introduced to define the category the entries displayed and added are referred to.

Including the comment function via template is like this:

APF template
<core:importdesign namespace="APF\modules\comments\pres\templates" template="comment" categorykey="****" />

The template addressed within the tag contains another template that decides whether to display the list or the form using a special feature of the <core:importdesign /> tag:

APF template
<core:importdesign namespace="APF\modules\comments\pres\templates" template="[coview = listing]" incparam="coview" />

In case the parameter coview is not present to the url or takes the value listing the list of entries are display. To generate the output the listing.html template is created:

APF template
<@controller class="APF\modules\comments\pres\controller\CommentListingController" @> <div class="cm--list"> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="listing.text.1" /> <a rel="nofollow" href="<html:placeholder name="Link" />#comments" title="<html:getstring namespace="APF\modules\comments" config="language.ini" entry="listing.text.2.title" />"><strong> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="listing.text.2" /> </strong></a> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="listing.text.3" /> </div> <html:template name="ArticleComment"> <div class="cm--list-item"> <div class="cm--list-item-head"> <div class="cm--list-item-head-num"><html:placeholder name="Number" /></div> <div class="cm--list-item-head-date"> <span><html:placeholder name="Name" /></span> <em><html:placeholder name="Date" />, <html:placeholder name="Time" /></em> </div> <div style="clear: left;"></div> </div> <div class="cm--list-item-body"> <html:placeholder name="Comment" /> </div> </div> </html:template> <html:template name="NoEntries"> <div class="cm--list-noentries"> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="noentries.text" /> </div> </html:template>

The listing template contains the document controller definition, the entry text, the pager, and the list of entries. Further, an additional template is defined that is displayed when no entries are found. Please refer to the Standard taglibs chapter for details on the tags used.

Having a look at the template inclusion using the <core:importdesign /> tag the list of attributes containes one more attribute compared to the standard tag: the category definition.

Since the Page controller creates a separate document within the DOM tree for each template or tag, the categorykey attribute is available within the appropriate DOM object. As you can take from the template structure, the inclusion of the list or form template is abstracted within the main template of the module once more. This structure is exactly applied to the DOM tree.

In order to access the desired DOM node from the document controller, you are able to retrieve a reference of the document the controller is responsible for. Starting at this, you can navigate through the tree as desired. Since the DOM node is the father node of the template the controller helps to transform the attribute of the <core:importdesign /> tag can be read like this:

PHP code
$parent = $this->getDocument()->getParent(); $categoryKey = $parent->getAttribute('categorykey', 'standard');

Because this functionality is used in both document controllers (creation of the list and addition of a new entry) it is added to a basic document controller. This class is located in the /APF/modules/comments/pres/controller/CommentBasecontrollerntroller.php file and is called CommentBasecontroller.

The document controller CommentListingController (see above template) is intended to display the category's comments along with the pager. The code is as follows:

PHP code
namespace APF\modules\comments\pres\controller; use APF\modules\comments\biz\ArticleComment; use APF\modules\comments\biz\ArticleCommentManager; use APF\tools\link\Url; use APF\tools\link\LinkGenerator; class CommentListingController extends CommentBaseDocumentController { public function transformContent() { /* @var $m ArticleCommentManager */ $m = $this->getAndInitServiceObject('APF\modules\comments\biz\ArticleCommentManager', $this->getCategoryKey()); $entries = $m->loadEntries(); $buffer = ''; $template = $this->getTemplate('ArticleComment'); $i = 1; foreach ($entries as $entry) { /* @var $entry ArticleComment */ $template->setPlaceHolder('Number', $i++); $template->setPlaceHolder('Name', $entry->getName()); $template->setPlaceHolder('Date', \DateTime::createFromFormat('Y-m-d', $entry->getDate())->format('d.m.Y')); $template->setPlaceHolder('Time', $entry->getTime()); $template->setPlaceHolder('Comment', $entry->getComment()); $buffer .= $template->transformTemplate(); } if (count($entries) < 1) { $Template__NoEntries = $this->getTemplate('NoEntries'); $buffer = $Template__NoEntries->transformTemplate(); } $this->setPlaceHolder('Content', $buffer); $this->setPlaceHolder('Pager', $m->getPager('comments')); $urlParams = $m->getURLParameter(); $this->setPlaceHolder('Link', LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array( $urlParams['PageName'] => '', $urlParams['CountName'] => '', 'coview' => 'form' ) ) ) ); } }

As you can take from the code bos the url parameters of the pager are reset during generation. This is because the first page should be displayed after adding a new entry.

3. Enhancements

The software described above is able to display comments generated by external tools. Creating entries is not implemented yet. For this reason the software should be added the possibility to write comments step-by-step. Enhancement of the software is done in this chapter in an top-down approach. So let's start with the presentation layer.

3.1. Presentation layer

In chapter 2.5 we already added the possibility to switch the sub template of the comment module using the coview url parameter. Now we create a new view for the form that is named form. As you can take from the domain object the user should be presented fields to enter

  • Name
  • Email
  • Comment

The view template is as follows:

APF template
<@controller class="APF\modules\comments\pres\controller\CommentCreateEntryController" @> <div class="cm--create"> <div class="cm--create-head"> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="formhint.text.1" /> <a href="<html:placeholder name="back" />#comments" title="<html:getstring namespace="APF\modules\comments" config="language.ini" entry="formhint.text.2.title" />"><strong><html:getstring namespace="APF\modules\comments" config="language.ini" entry="formhint.text.2" /></strong></a><html:getstring namespace="APF\modules\comments" config="language.ini" entry="formhint.text.3" /> </div> <div class="cm--create-form"> <html:form name="AddComment" method="post"> <span><html:getstring namespace="APF\modules\comments" config="language.ini" entry="" />*</span> <form:text maxlength="100" name="Name" class="cm--create-element-name" /> <br /> <span><html:getstring namespace="APF\modules\comments" config="language.ini" entry="" />*</span> <form:text maxlength="100" name="EMail" class="cm--create-element-email" /> <br /> <br /> <html:getstring namespace="APF\modules\comments" config="language.ini" entry="form.comment" /> <br /> <form:area name="Comment" class="cm--create-element-comment" cols="50" rows="6" /> <br /> <br /> <span><html:getstring namespace="APF\modules\comments" config="language.ini" entry="form.confirm" />*</span> <br /> <br /> <form:captcha text_class="cm--create-element-captcha" name="Captcha" clearonerror="true" /> <br /> <br /> <form:button name="Save" class="cm--create-element-button" /> <form:addvalidator class="APF\tools\form\validator\TextLengthValidator" button="Save" control="Name|Comment" /> <form:addvalidator class="APF\tools\form\validator\EMailValidator" button="Save" control="EMail" /> <form:addfilter class="APF\tools\form\filter\EMailFilter" button="Save" control="EMail" /> <form:addvalidator class="APF\modules\captcha\pres\validator\CaptchaValidator" control="Captcha" button="Save" /> </html:form> </div> </div>

The document controller belonging to this template file (CommentCreateEntryController) generates the form and controls the submission. In case the form is filled correctly the controller uses the business component to store the new entry. To be able to do this, the manager must be added the saveEntry() method that takes a domain object as an argument. The code box shows the complete PHP code:

PHP code
namespace APF\modules\comments\pres\controller; use APF\modules\comments\biz\ArticleComment; use APF\modules\comments\biz\ArticleCommentManager; use APF\tools\link\LinkGenerator; use APF\tools\link\Url; class CommentCreateEntryController extends CommentBaseDocumentController { public function transformContent() { $form = $this->getForm('AddComment'); if ($form->isSent() == true) { /* @var $m ArticleCommentManager */ $m = $this->getAndInitServiceObject('APF\modules\comments\biz\ArticleCommentManager', $this->getCategoryKey()); if ($form->isValid() == true) { $articleComment = new ArticleComment(); $name = $form->getFormElementByName('Name'); $articleComment->setName($name->getAttribute('value')); $email = $form->getFormElementByName('EMail'); $articleComment->setEmail($email->getAttribute('value')); $comment = $form->getFormElementByName('Comment'); $articleComment->setComment($comment->getContent()); $m->saveEntry($articleComment); } else { $this->buildForm(); } } else { $this->buildForm(); } } private function buildForm() { $form = $this->getForm('AddComment'); $form->setAttribute('action', $_SERVER['REQUEST_URI'] . '#comments'); $config = $this->getConfiguration('APF\modules\comments', 'language.ini'); $button = $form->getFormElementByName('Save'); $button->setAttribute('value', $config->getSection($this->getLanguage())->getValue('form.button')); $form->transformOnPlace(); $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array('coview' => 'listing'))); $this->setPlaceHolder('back', $link); } }

3.2. Business layer

In this chapter the task is to implement the saveEntry() function that was described in the section before. Merely, the domain object must be saved and the corresponding view must be displayed. As with loading entries, for this task the data layer component is considered. To be able to proceed with the top-down-approach, we assume the mapper to have the saveArticleComment() method allowing the manager to save the entry. Displaying the right view after saving is done with a simple redirect. Creating the url you must be careful to display the correct page.

PHP code
public function saveEntry(ArticleComment $articleComment) { /* @var $M ArticleCommentMapper */ $M = $this->getServiceObject('APF\modules\comments\data\ArticleCommentMapper'); $articleComment->setCategoryKey($this->categoryKey); $M->saveArticleComment($articleComment); $link = LinkGenerator::generateUrl( Url::fromCurrent() ->mergeQuery(array('coview' => 'listing')) ->setAnchor('comments') ); $this->getResponse()->forward($link); }

As line 5 ($articleComment->setCategoryKey(...)) shows us the business component manipulates the domain object to save it within the correct category. Thereafter, the link is generated using the LinkGenerator::generateUrl() method as we have already seen for the form.

3.3. Data layer

Within the data layer the saveArticleComment() method now must be implemented:

PHP code
public function saveArticleComment(ArticleComment $comment) { $conn = $this->getConnection(); if ($comment->getId() == null) { $insert = 'INSERT INTO article_comments (Name, EMail, Comment, Date, Time, CategoryKey) VALUES (\'' . $conn->escapeValue($comment->getName()) . '\',\'' . $conn->escapeValue($comment->getEmail()) . '\',\'' . $conn->escapeValue($comment->getComment()) . '\',CURDATE(),CURTIME(),\'' . $comment->getCategoryKey() . '\');'; $conn->executeTextStatement($insert); } }

The code is really simple: fist, the database connection is established using the ConnectionManager. Second, the comment is saved as relational structure after a check.

4. Perspective / additions

As a follow-up we recommend the article Objektorientiertes Design eines Gästebuchs to learn more about creating software with the APF.

Concerning the implementation, please note that the getServiceObject() and getAndInitServiceObject() of the ServiceManager are responsible for injecting the necessary information (e.g. language, context, ...) into the service components. Thus, it is recommended to always create services this way to be sure to have access to these information.


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.
« 1   »
Entries/Page: | 5 | 10 | 15 | 20 |
Christian 01.11.2009, 21:31:43
Hello giobien5, the source code of the comment module is included in the APF release. You can find the code in the [i]apps/modules/kontakt4/[/i] folder.
giobien5 24.10.2009, 09:25:29
please upload source for everyone demo visualy

In order to provide a state-of-the-art web experience and to continuously improve our services we are using cookies. By using this web page you agree to the use of cookies. For more information, please refer to our Privacy policy.