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 /apps/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 (documentcontroller) and one for the view templates (templates). The complete structure is as follows:

/apps /config /core /extensions /modules /comments /biz /data /pres /css /documentcontroller /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 /apps/modules/comments/biz and contains the following content:

PHP code
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 /apps/modules/comments/data with the content displayed within the next code box:

PHP code
import('modules::comments::biz', 'ArticleComment'); class commentMapper 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() { $cM = &$this->getServiceObject('core::database', 'ConnectionManager'); $config = $this->getConfiguration('modules::comments', 'comments.ini'); $connectionKey = $config->getSection('Default')->getValue('Database.ConnectionKey'); if ($connectionKey == null) { throw new InvalidArgumentException('[commentMapper::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:

  • The import() function includes dependent classes. This is the ArticleComment domain object. The ConnectionManager is automatically included for you, since it is created by the ServiceManager.
  • 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 /apps/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 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 /apps/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
class commentManager 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 talk about the integration of the Pager. The pager is normally created using the PagerManagerFabric that needs the configuration section to initialize the pager:

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

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

APF configuration
[ArticleComments] Pager.EntriesPerPage = "5" Pager.ParameterPageName = "PgrPg" Pager.ParameterCountName = "PgrAnz" Pager.StatementNamespace = "modules::comments" Pager.CountStatement = "load_entries_count.sql" Pager.CountStatement.Params = "CategoryKey:standard" Pager.EntriesStatement = "load_entry_ids.sql" Pager.EntriesStatement.Params = "CategoryKey:standard" Pager.DesignNamespace = "modules::pager::pres::templates" Pager.DesignTemplate = "pager_2" Pager.DatabaseConnection = "MySQLx" Pager.AllowDynamicEntriesPerPage = "true"

Details on the configuration mechanism can be taken from Configuration.

With the above set of parameters the pager can be fully configured. For a start, you can take the /config/modules/comments/EXAMPLE_comments.ini file from the apf-configpack-* release package.

The first three directives define the amount of entries per page and the names of the url parameters to use. Further, the statements are defined required by the pager to execute it's job. Here, the first one is intended to get the amount of total entries, the second one to load the ids of the entries to display. The *.Params directives define the default statement parameters.

As described under Configuration the file name is prepended the environment variable value. In case of our SQL statement files, the names are:

  • DEFAULT_load_entries_count.sql
  • DEFAULT_load_entry_ids.sql

Pager statement files are subjected to convention concerning the naming of the dynamic parameters. Loading the amount of entries within one category, the content is as described in the next code box:

SQL statement
SELECT COUNT(*) AS EntriesCount FROM article_comments WHERE CategoryKey = '[CategoryKey]' GROUP BY CategoryKey;

The pager expects the result of the query to be stored within the EntriesCount variable. For this reason, an alias must be used. The category is passed to the statement as dynamic variable CategoryKey and is thus noted in brackets.

The statement to load the entries looks like this:

SQL statement
SELECT ArticleCommentID AS DB_ID FROM article_comments WHERE CategoryKey = '[CategoryKey]' ORDER BY Date DESC, Time DESC LIMIT [Start],[EntriesCount];

As above, the pager expects the result id of the statement within the DB_ID variable. This is realized using a alias, too. The category, the start point, and the amount of entries are passed as variables in bracket notation as well. In order to limit the result, the LIMIT clause can be filled with the parameters

  • [Start]
  • [EntriesCount]

They are automatically filled by the pager to only display those entries that are desired for the present page. Beside these elements the statement can be as complex as desired.

After having finished with the configuration of the pager, we can implement the loadEntries() method of the manager:

PHP code
public function loadEntries() { $pMF = &$this->getServiceObject('modules::pager::biz', 'PagerManagerFabric'); $pM = &$pMF->getPagerManager('ArticleComments'); $M = &$this->getServiceObject('modules::comments::data', 'commentMapper'); return $pM->loadEntriesByAppDataComponent($M, 'loadArticleCommentByID', array('CategoryKey' => $this->categoryKey)); }

First of all, the desired pager instance is created using the PagerManagerFabric. Second, the application's data mapper is created and passed to the pager to 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(){ $pMF = &$this->getServiceObject('modules::pager::biz','PagerManagerFabric'); $pM = &$pMF->getPagerManager('ArticleComments'); $entries = $pM->loadEntries(array('CategoryKey' => $this->categoryKey)); $M = &$this->getServiceObject('modules::comments::data','commentMapper'); $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="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="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 namespace="modules::comments::pres::documentcontroller" class="CommentListingController" @> <div class="cm--list"> <div class="cm--list-head"> <html:getstring namespace="modules::comments" config="language" entry="listing.text.1" /> <a rel="nofollow" href="<html:placeholder name="Link" />#comments" title="<html:getstring namespace="modules::comments" config="language" entry="listing.text.2.title" />"><strong><html:getstring namespace="modules::comments" config="language" entry="listing.text.2" /></strong></a> <html:getstring namespace="modules::comments" config="language" entry="listing.text.3" /> </div> <div class="cm--list-pager"> <html:placeholder name="Pager" /> </div> <div class="cm--list-items"> <html:placeholder name="Content" /> </div> </div> <html:template name="ArticleComment"> <div class="cm--list-item"> <div class="cm--list-item-head"> <div class="cm--list-item-head-num"><template:placeholder name="Number" /></div> <div class="cm--list-item-head-date"> <span><template:placeholder name="Name" /></span> <em><template:placeholder name="Date" />, <template:placeholder name="Time" /></em> </div> <div style="clear: left;"></div> </div> <div class="cm--list-item-body"> <template:placeholder name="Comment" /> </div> </div> </html:template> <html:template name="NoEntries"> <div class="cm--list-noentries"> <template:getstring namespace="modules::comments" config="language" 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()->getParentObject(); $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 /apps/modules/comments/pres/documentcontroller/CommentBaseDocumentControllerntroller.php file and is called CommentBaseDocumentController.

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
class CommentListingController extends CommentBaseDocumentController { public function transformContent() { $M = &$this->getAndInitServiceObject('modules::comments::biz', 'commentManager', $this->getCategoryKey()); $entries = $M->loadEntries(); $buffer = (string) ''; $template = &$this->getTemplate('ArticleComment'); $bP = &$this->getServiceObject('tools::string', 'AdvancedBBCodeParser'); $bP->removeProvider('standard.font.color'); $bP->removeProvider('standard.font.size'); $i = 1; foreach ($entries as $entry) { $template->setPlaceHolder('Number', $i++); $template->setPlaceHolder('Name', $entry->getName()); $template->setPlaceHolder('Date', date('d.m.Y', strtotime($entry->getDate()))); $template->setPlaceHolder('Time', $entry->getTime()); $template->setPlaceHolder('Comment', $bP->parseCode($entry->getComment())); $buffer .= $template->transformTemplate(); } if (count($entries) < 1) { $templateNoEntries = &$this->getTemplate('NoEntries'); $buffer = $templateNoEntries->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 such as PHPMyAdmin. Creating entries is not implemented yet. For this reason the software should be added the possibility to write comments step-by-step. The upgrade to the software is now done in a top-down approach. So let's start with the presentation layer.

3.1. Presentation layer

In chapter 2.4 we 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 namespace="modules::comments::pres::documentcontroller" class="CommentCreateEntryController" @> <core:addtaglib namespace="tools::form::taglib" class="HtmlFormTag" prefix="html" name="form" /> <div class="cm--create"> <div class="cm--create-head"> <html:getstring namespace="modules::comments" config="language" entry="formhint.text.1" /> <a href="<html:placeholder name="back" />#comments" title="<html:getstring namespace="modules::comments" config="language" entry="formhint.text.2.title" />"><strong><html:getstring namespace="modules::comments" config="language" entry="formhint.text.2" /></strong></a><html:getstring namespace="modules::comments" config="language" entry="formhint.text.3" /> </div> <div class="cm--create-form"> <html:form name="AddComment" method="post"> <span><form:getstring namespace="modules::comments" config="language" entry="" />*</span> <form:text maxlength="100" name="Name" class="cm--create-element-name" /> <form:addvalidator class="TextLengthValidator" button="Save" control="Name|Comment" /> <br /> <span><form:getstring namespace="modules::comments" config="language" entry="" />*</span> <form:text maxlength="100" name="EMail" class="cm--create-element-email" /> <form:addvalidator class="EMailValidator" button="Save" control="EMail" /> <form:addfilter class="EMailFilter" button="Save" control="EMail" /> <br /> <br /> <form:getstring namespace="modules::comments" config="language" entry="form.comment" /> <br /> <form:area name="Comment" class="cm--create-element-comment" cols="50" rows="6" /> <br /> <br /> <span><form:getstring namespace="modules::comments" config="language" entry="form.confirm" />*</span> <br /> <br /> <form:addtaglib namespace="modules::captcha::pres::taglib" class="SimpleCaptchaTag" prefix="form" name="captcha" /> <form:captcha text_class="cm--create-element-captcha" name="Captcha" clearonerror="true" /> <form:addvalidator namespace="modules::captcha::pres::validator" class="CaptchaValidator" control="Captcha" button="Save" /> <br /> <br /> <form:button name="Save" class="cm--create-element-button" /> </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
class CommentCreateEntryController extends CommentBaseDocumentController { public function transformContent() { $form = &$this->getForm('AddComment'); if ($form->isSent() == true) { $M = &$this->getAndInitServiceObject('modules::comments::biz', 'commentManager', $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('modules::comments', 'language.ini'); $button = &$form->getFormElementByName('Save'); $button->setAttribute('value', $config->getSection($this->language)->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) { $M = &$this->getServiceObject('modules::comments::data', 'commentMapper'); $articleComment->setCategoryKey($this->categoryKey); $M->saveArticleComment($articleComment); $link = LinkGenerator::generateUrl(Url::fromCurrent()->mergeQuery(array('coview' => 'listing'))); HeaderManager::forward($link . '#comments'); }

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 |
cheap_cialis 04.11.2016, 12:04:25
You can pay for cheap cialis at the website tablets-au.
via_gra_uk 15.09.2016, 10:52:31
I have read several good stuff here. Certainly worth bookmarking for revisiting. I surprise how much effort you put to make such a magnificent informative web site.
viagra_uk 15.09.2016, 10:52:20
I have read several good stuff here. Certainly worth bookmarking for revisiting. I surprise how much effort you put to make such a magnificent informative web site.
viagra_uk 15.09.2016, 10:51:51

viagra uk was originally studied for the use in people with high blood pressure and cardiac problems .
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 apps/modules/kontakt4/ folder.
giobien5 24.10.2009, 09:25:29
please upload source for everyone demo visualy