This is a guide for creating a contact form using Code Jetter Framework. This also helps you to get familiar yourself with the framework. I will break this down to small steps and explain them all in details. But before diving into the code follow this guide if you have not installed Code Jetter yet. Otherwise let’s get started with the step 1 which addresses the requirements.
1. Requirements
First of all, we need to think what needs to be done or in other words what the requirements are. Basically there should be an HTML form to receive any messages from users and also back-end functions to store them into database. Additionally, there should be a list for the admin to manage the messages. To achieve this we need to create a new component called Contact
in Code Jetter.
2. Adding The New Component
Basically, adding a new component to Code Jetter includes:
– A folder and sub-folders in CodeJetter/components
. In this case contact
folder and controllers
, mappers
, models
and templates
sub-folders
– Routes which should be specified in CodeJetter/Routes.php
– A controller to keep the application logic. In this case ContactController
which has got functions such as index
, newMessage
, lisMessages
, etc and is located in CodeJetter/components/contact/controllers
– A model to represent a data entity. In this case ContactMessage
which is located in CodeJetter/components/contact/models
– A mapper to map raw data to the relevant model. In this case ContactMessageMapper
which is located in CodeJetter/components/contact/mappers
– Templates that are used for presentations.
3. HTML Form
In order to display the HTML form first we need to specify a simple
route with GET
request in CodeJetter/Routes.php
:
1 2 3 4 |
'/contact' => [ 'component' => 'contact', 'controller' => 'Contact' ] |
This route means that every GET request to YOUR APP URL/contact
will be sent to index
function in CodeJetter/components/contact/controllers/ContactController.php
. Please note that in CodeJetter/Config.php
, defaultAction
and defaultRole
are set to index
and public
respectively. In this case we stick to the default values and do not specify them again. So let’s add index
to ContactController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php namespace CodeJetter\components\contact\controllers; use CodeJetter\core\BaseController; /** * Class ContactController * @package CodeJetter\components\contact\controllers */ class ContactController extends BaseController { /** * @throws \Exception */ public function index() { } } |
At this point, to check if you have done the steps correctly place the following code in index
function, and check YOUR APP URL/contact
in the browser:
1 |
var_dump('If you see this, you're ready to rock!');exit; |
If you see this message in the browser it means that routing works as expected. Now we need to prepare different elements of the view including page
, language
, component template
and form handler
objects. So the controller will be like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<?php namespace CodeJetter\components\contact\controllers; use CodeJetter\components\page\models\Page; use CodeJetter\core\BaseController; use CodeJetter\core\FormHandler; use CodeJetter\core\layout\blocks\ComponentTemplate; use CodeJetter\core\Registry; use CodeJetter\core\View; /** * Class ContactController * @package CodeJetter\components\contact\controllers */ class ContactController extends BaseController { /** * @throws \Exception */ public function index() { $page = new Page($this->getRouteInfo()->getAccessRole()); $page->setTitle('Contact'); /** * hi to language */ $language = Registry::getLanguageClass(); $requiredFields = $language->get('requiredFields'); /** * bye to language */ $componentTemplate = new ComponentTemplate(); $componentTemplate->setTemplatePath('/components/contact/templates/contactForm.php'); $componentTemplate->setData([ 'requiredFields' => $requiredFields ]); (new View())->make( $page, [ 'contact' => $componentTemplate ], null, new FormHandler('Contact') ); } } |
Each view must have a page
object which is used for different page related properties such as title and meta tags. By passing access role to page constructor, meta tag is automatically set based on accessRolesRobot
in the config. Also using a language object you can get the translations for different keys in this case requiredFields
. Default language is set in the config and the language JSON files are located in core/language
. Then as you see, component template object has been specified. Each view can have one to many component templates. A component template must have a template path and can contain data. This is used to pass data to the view in this case requiredFields
. Lastly we need to create the template /components/contact/templates/contactForm.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
<?php /** @var CodeJetter\core\FormHandler $formHandler */ $data = $this->getCurrentComponentTemplate()->getData(); return "<div class='container-fluid'> <div class='row'> <div class='col-md-6 col-md-offset-3'> <form role='form' data-url='/contact/new' data-reset-on-success='true'> <div class='row'> <div class='col-md-12'> <ul class='bg-info form-description'> <li>{$data['requiredFields']}</li> </ul> </div> </div> <div class='row'> <div class='col-md-6'> <div class='form-group'> <label for='name' class='control-label'>Name</label> <input type='text' class='form-control' name='name' id='name' placeholder='Name' value='' autocomplete='false'> </div> </div> <div class='col-md-6'> <div class='form-group'> <label for='email' class='control-label'>Email *</label> <input type='text' class='form-control' name='email' id='email' placeholder='Email' value=''> </div> </div> </div> <div class='row'> <div class='col-md-12'> <div class='form-group'> <label for='message' class='control-label'>Message *</label> <textarea type='text' class='form-control' name='message' id='message' placeholder='Enter you message ...'></textarea> </div> </div> </div> <div class='form-group'> {$formHandler->generateAntiCSRFHtml()} <button type='submit' class='btn btn-success'>Send</button> </div> </form> </div> </div> </div>"; |
As you see $this->getCurrentComponentTemplate()->getData();
is used to get data for the current template. Also since we are going to send AJAX requests, data-url='/contact/new'
is added to the form tag and it is handled in CodeJetter/public/scripts/script.js
. To check everything is fine load YOUR APP URL/contact
in the browser again and the result should be the same as below:
4. Storing Messages In Database
To store a message upon submission we start with creating the ContactMessage
model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
<?php namespace CodeJetter\components\contact\models; use CodeJetter\core\BaseModel; class ContactMessage extends BaseModel { private $name; private $email; private $message; /** * @return string */ public function getName() { return $this->name; } /** * @param string $name */ public function setName($name) { $this->name = $name; } /** * @return string */ public function getMessage() { return $this->message; } /** * @param string $message */ public function setMessage($message) { $this->message = $message; } /** * @return string */ public function getEmail() { return $this->email; } /** * @param string $email */ public function setEmail($email) { $this->email = $email; } } |
As you see ContactMessage
class extends BaseModel
class and has got name, email and message properties with all the getters and setters. Now let’s create the mapper that has got the following structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace CodeJetter\components\contact\mappers; use CodeJetter\core\BaseMapper; class ContactMessageMapper extends BaseMapper { public function add(array $inputs) { // TODO: Implement add() method. } } |
As you see ContactMessageMaper
class extends BaseMapper
class and because BaseMapper
implements ICrud
interface, ContactMessageMaper
must have add
function. Also each mapper name must be model name with Mapper
suffix. To insert data into database first of all inputs need to be validated. This can be achieved using Code Jetter Validator
class. To use it, expected inputs (Input
objects that can have one to many ValidatorRule
s) and the user (external) inputs must be passed to the constructor. Finally validate
function should be called on $validator
object:
1 2 3 4 5 6 7 8 9 10 11 |
$requiredRule = new ValidatorRule('required'); $emailRule = new ValidatorRule('email'); $definedInputs = [ new Input('name'), new Input('email', [$requiredRule, $emailRule]), new Input('message', [$requiredRule]) ]; $validator = new Validator($definedInputs, $inputs); $validatorOutput = $validator->validate(); |
Finally, all the columns and values should be passed to insertOne
which is located in BaseMapper
and handles inserting to database using PDO
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$name = isset($inputs['name']) ? $inputs['name'] : ''; $fieldsValues = [ [ 'column' => 'name', 'value' => $name, ], [ 'column' => 'email', 'value' => $inputs['email'], ], [ 'column' => 'message', 'value' => $inputs['message'], ], ]; $insertedId = $this->insertOne($fieldsValues); |
So the final mapper will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<?php namespace CodeJetter\components\contact\mappers; use CodeJetter\core\BaseMapper; use CodeJetter\core\io\Input; use CodeJetter\core\io\Output; use CodeJetter\core\security\Validator; use CodeJetter\core\security\ValidatorRule; class ContactMessageMapper extends BaseMapper { public function add(array $inputs) { /** * Start validating */ $output = new Output(); try { $requiredRule = new ValidatorRule('required'); $emailRule = new ValidatorRule('email'); $definedInputs = [ new Input('name'), new Input('email', [$requiredRule, $emailRule]), new Input('message', [$requiredRule]) ]; $validator = new Validator($definedInputs, $inputs); $validatorOutput = $validator->validate(); if ($validatorOutput->getSuccess() !== true) { $output->setSuccess(false); $output->setMessages($validatorOutput->getMessages()); return $output; } } catch (\Exception $e) { (new \CodeJetter\core\ErrorHandler())->logError($e); } /** * Finish validating */ /** * Start inserting */ $name = isset($inputs['name']) ? $inputs['name'] : ''; $fieldsValues = [ [ 'column' => 'name', 'value' => $name, ], [ 'column' => 'email', 'value' => $inputs['email'], ], [ 'column' => 'message', 'value' => $inputs['message'], ], ]; $insertedId = $this->insertOne($fieldsValues); $output = new Output(); if (!empty($insertedId) && is_numeric($insertedId) && (int) $insertedId > 0) { $output->setSuccess(true); $output->setData($insertedId); } else { $output->setSuccess(false); } /** * Finish inserting */ return $output; } } |
We also need to create the MySQL table called cj_contact_messages
to be able to store the messages. To do that run the following query in your Code Jetter database:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
-- -- Table structure for table `cj_contact_messages` -- CREATE TABLE `cj_contact_messages` ( `id` int(11) unsigned NOT NULL, `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, `email` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `message` text COLLATE utf8_unicode_ci NOT NULL, `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `modifiedAt` timestamp NULL DEFAULT NULL, `live` enum('1') COLLATE utf8_unicode_ci DEFAULT '1', `archivedAt` timestamp NULL DEFAULT NULL ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -- -- Triggers `cj_contact_messages` -- DELIMITER // CREATE TRIGGER `updateModifiedAtContactMessages` BEFORE UPDATE ON `cj_contact_messages` FOR EACH ROW SET NEW.modifiedAt = CURRENT_TIMESTAMP // DELIMITER ; -- -- Indexes for dumped tables -- -- -- Indexes for table `cj_contact_messages` -- ALTER TABLE `cj_contact_messages` ADD PRIMARY KEY (`id`); -- -- AUTO_INCREMENT for dumped tables -- -- -- AUTO_INCREMENT for table `cj_contact_messages` -- ALTER TABLE `cj_contact_messages` MODIFY `id` int(11) unsigned NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=1; |
Please note that cj
is the prefix that is specified in the config (tablePrefix
). Also since the mapper name is named ContactMessageMapper
Code Jetter expects to have a table named cj_contact_messages
. If table name is something else you need to specify it in the config (mapperTableRelations
). Please remember that each table must have createdAt
, modifiedAt
, live
and archivedAt
columns. Also updateModifiedAtContactMessages
trigger is used to update modifiedAt
when the row gets updated. Since MySQL table cannot have multiple timestamps in old versions (only in MySQL 5.6.+ multiple timestamps are allowed), the trigger is used for backwards compatibility.
Now that ContactMessage
and ContactMessageMapper
are ready we can specify a simple
route with POST
request in CodeJetter/Routes.php
:
1 2 3 4 5 |
'/contact/new' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'newMessage' ] |
This route means that every POST request to YOUR APP URL/contact/new
will be sent to newMessage
function in CodeJetter/components/contact/controllers/ContactController.php
. Well the last step is the controller and it should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
<?php namespace CodeJetter\components\contact\controllers; use CodeJetter\components\contact\mappers\ContactMessageMapper; use CodeJetter\components\page\models\Page; use CodeJetter\core\BaseController; use CodeJetter\core\FormHandler; use CodeJetter\core\io\Output; use CodeJetter\core\io\Request; use CodeJetter\core\io\Response; use CodeJetter\core\layout\blocks\ComponentTemplate; use CodeJetter\core\Registry; use CodeJetter\core\View; /** * Class ContactController * @package CodeJetter\components\contact\controllers */ class ContactController extends BaseController { /** * @throws \Exception */ public function index() { $page = new Page($this->getRouteInfo()->getAccessRole()); $page->setTitle('Contact'); /** * hi to language */ $language = Registry::getLanguageClass(); $requiredFields = $language->get('requiredFields'); /** * bye to language */ $componentTemplate = new ComponentTemplate(); $componentTemplate->setTemplatePath('/components/contact/templates/contactForm.php'); $componentTemplate->setData([ 'requiredFields' => $requiredFields ]); (new View())->make( $page, [ 'contact' => $componentTemplate ], null, new FormHandler('Contact') ); } public function newMessage() { $inputs = (new Request('POST'))->getInputs(); $output = (new ContactMessageMapper())->add($inputs); if ($output->getSuccess() === true) { /** * hi to language */ $language = Registry::getLanguageClass(); $successfulSubmission = $language->get('successfulContactMessageSubmission'); /** * bye to language */ // do not expose anything to world, that's why another output is created $output->setData(''); $output->setSuccess(true); $output->setMessage($successfulSubmission); } (new Response())->echoContent($output->toJSON()); } } |
As you see, we get the inputs using Request
class in newMessage
. Then they are passed to add
function in ContactMessageMapper
. Also you need to add successfulContactMessageSubmission
to CodeJetter/core/language/en.json
:
1 2 3 4 5 6 7 8 9 |
{ "language": "en", "testLanguage": "en, {placeholder1}, {placeholder2}, {placeholder1}", "requiredFields": "All * fields are required", "passwordRequirements": "Password must be 6 to 20 characters, at least 1 lowercase, 1 uppercase, 1 number", "usernameRequirements": "Username must be 3 to 20 characters, and cannot start with _", "uniqueField": "{field} must be unique", "successfulContactMessageSubmission": "Your message submitted successfully" } |
Finally, the output is sent to font-end using Response
class.
5. Contact Messages Management
So far we have managed to display the contact form and store validated data in a MySQL database. Now we need to have a list in admin (private) area to give the admins ability to view and manage messages. The same as before we need to have some routes, functions in the controller and a template for this. So first of all add the following routes to the router:
– simple
GET
route which is used to display the list
1 2 3 4 5 |
'/admin/contact/messages' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'listMessages' ], |
– regex
GET
route which is also used to display the list specially with a pager
1 2 3 4 5 6 |
'/admin/contact/messages/page/{page:int}/limit/{limit:int:?}' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'listMessages', 'base' => '/admin/contact/messages' ], |
– simple
POST
routes which are used to delete the messages
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
'/admin/contact/safe-delete-message' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'safeDeleteMessage' ], '/admin/contact/safe-batch-delete-message' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'safeBatchDeleteMessage' ], '/admin/contact/delete-message' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'deleteMessage' ], '/admin/contact/batch-delete-message' => [ 'component' => 'contact', 'controller' => 'Contact', 'action' => 'batchDeleteMessage' ], |
As you see there are some actions or actually functions that we need to create in ContactController
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
/** * @throws \Exception */ public function listMessages() { // for pagination // for simple router url path is enough, for regex ones base path is needed $pager = new Pager($this->getURLParameters(), $this->getBasePath(), $this->getRouteInfo()->getUrl()); $numberCell = new HeadCell('#'); $numberCell->setSortable(false); $numberCell->setSearchable(false); $actionsCell = new HeadCell('Actions'); $actionsCell->setSortable(false); $actionsCell->setSearchable(false); $listHeaders = [ $numberCell, new HeadCell('Name'), new HeadCell('Email'), new HeadCell('Message'), $actionsCell ]; // get order and search query from the query string $listConfig = Registry::getConfigClass()->get('list'); $request = new Request(); $searchQuery = $request->getQueryStringVariables($listConfig['query']); $order = $request->getSortingFromQueryString(); $criteria = (new MysqlUtility())->generateSearchCriteria($listHeaders); $output = (new ContactMessageMapper())->getAll( $criteria, [], $order, $pager->getStart(), $pager->getLimit(), true ); /** * If the current page is not page 1, and there is no data, * set pager to page 1 and get data again with start 0 */ if (empty($output['result']) && $output['total'] > 0 && $pager->getCurrentPage() > 1) { $pager->setCurrentPage(1); $output = (new ContactMessageMapper())->getAll($criteria, [], $order, 0, $pager->getLimit(), true); } $pager->setTotal($output['total']); // create component template $componentTemplate = new ComponentTemplate(); $componentTemplate->setTemplatePath('components/contact/templates/contactMessageList.php'); $componentTemplate->setPager($pager); $componentTemplate->setData([ 'listHeaders' => $listHeaders, 'messages' => $output['result'], 'searchQueryKey' => $listConfig['query'], 'searchQuery' => $searchQuery ]); // create the page for view $page = new Page($this->getRouteInfo()->getAccessRole()); $page->setTitle('Contact Message List'); (new View())->make( $page, [ 'messages' => $componentTemplate ], null, new FormHandler('List Messages') ); } /** * Safely delete a message * * @throws \Exception */ public function safeDeleteMessage() { $inputs = (new Request('POST'))->getInputs(); if (!empty($inputs['id'])) { $output = (new ContactMessageMapper())->safeDeleteOne([ [ 'column' => 'id', 'value' => $inputs['id'], 'type' => \PDO::PARAM_INT ] ]); if ($output > 0) { $output = new Output(); $output->setSuccess(true); $output->setMessage('Message safely deleted'); (new Response())->echoContent($output->toJSON()); } } } /** * Safely batch delete messages * * @throws \Exception */ public function safeBatchDeleteMessage() { $inputs = (new Request('POST'))->getInputs(); /** * Start validating */ $output = new Output(); if (empty($inputs['callback'])) { $output->setSuccess(false); $output->setMessage('Please select messages first'); (new Response())->echoContent($output->toJSON()); } // convert to array if it is not array if (!is_array($inputs['callback'])) { $inputs['callback'] = explode(',', $inputs['callback']); } $requiredRule = new ValidatorRule('required'); $idRule = new ValidatorRule('id'); $idInput = new Input('id', [$requiredRule, $idRule]); $definedInputs = [ $idInput ]; foreach ($inputs['callback'] as $id) { $validatorOutput = (new Validator($definedInputs, ['id' => $id]))->validate(); if ($validatorOutput->getSuccess() !== true) { $output->setSuccess(false); $output->setMessages($validatorOutput->getMessages()); (new Response())->echoContent($output->toJSON()); } } /** * Finish validating */ $output = (new ContactMessageMapper())->safeDelete([ [ 'column' => 'id', 'operator' => 'IN', 'value' => $inputs['callback'], 'type' => \PDO::PARAM_INT ] ]); if ($output > 0) { $output = new Output(); $output->setSuccess(true); $output->setMessage('Message safely deleted'); (new Response())->echoContent($output->toJSON()); } } /** * Delete a message * * @throws \Exception */ public function deleteMessage() { $inputs = (new Request('POST'))->getInputs(); if (!empty($inputs['id'])) { /** * Do not need to delete xref, since foreign key is set to CASCADE on delete */ $output = (new ContactMessageMapper())->deleteOne([ [ 'column' => 'id', 'value' => $inputs['id'], 'type' => \PDO::PARAM_INT ] ]); if ($output == 'true') { $output = new Output(); $output->setSuccess(true); $output->setMessage('Message deleted'); (new Response())->echoContent($output->toJSON()); } } } /** * Batch delete messages * * @throws \Exception */ public function batchDeleteMessage() { $inputs = (new Request('POST'))->getInputs(); /** * Start validating */ $output = new Output(); if (empty($inputs['callback'])) { $output->setSuccess(false); $output->setMessage('Please select messages first'); (new Response())->echoContent($output->toJSON()); } // convert to array if it is not array if (!is_array($inputs['callback'])) { $inputs['callback'] = explode(',', $inputs['callback']); } $requiredRule = new ValidatorRule('required'); $idRule = new ValidatorRule('id'); $idInput = new Input('id', [$requiredRule, $idRule]); $definedInputs = [ $idInput ]; foreach ($inputs['callback'] as $id) { $validatorOutput = (new Validator($definedInputs, ['id' => $id]))->validate(); if ($validatorOutput->getSuccess() !== true) { $output->setSuccess(false); $output->setMessages($validatorOutput->getMessages()); (new Response())->echoContent($output->toJSON()); } } /** * Finish validating */ $output = (new ContactMessageMapper())->delete([ [ 'column' => 'id', 'operator' => 'IN', 'value' => $inputs['callback'], 'type' => \PDO::PARAM_INT ] ]); if ($output == 'true') { $output = new Output(); $output->setSuccess(true); $output->setMessage('Message(s) deleted'); (new Response())->echoContent($output->toJSON()); } } |
listMessages
contains a pager which is used to manage pagination of the list, headers that are used to set sortable and searchable list heads, request functions to handle search and sort features, and component template that has got a template: components/contact/templates/contactMessageList.php
. Regarding the delete functions you may have noticed that all the jobs related to database are handled using mapper class. Luckily, BaseMapper
can handle all the delete functions and you do not need to add anything to ContactMessageMapper
. But you may ask the difference between delete
and safeDelete
. As you may have guessed, the first one permanently removes the record from database whereas the second one sets live
from 1 to NULL and archivedAt
in the table. So the last step is to create the template mentioned in listMessages
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
<?php use CodeJetter\core\utility\HtmlUtility; use CodeJetter\core\utility\StringUtility; use CodeJetter\libs\TableGenerator\Cell; use CodeJetter\libs\TableGenerator\HeadCell; use CodeJetter\libs\TableGenerator\Row; use CodeJetter\libs\TableGenerator\Head; use CodeJetter\libs\TableGenerator\Body; use CodeJetter\libs\TableGenerator\Table; /** @var CodeJetter\core\FormHandler $formHandler */ /** @var CodeJetter\core\View $this */ $currentPage = $this->getCurrentComponentTemplate()->getPager()->getCurrentPage(); $data = $this->getCurrentComponentTemplate()->getData(); $messages = $data['messages']; $searchQuery = $data['searchQuery']; $searchQueryKey = $data['searchQueryKey']; /** * replace the first element (#) with custom html */ $numberHeadCell = $data['listHeaders'][0]; if ($numberHeadCell instanceof HeadCell) { $numberHeadCellContent = "<div class='btn-group'> <a type='button' class='btn btn-primary' onclick=\"checkAll(this, 'input[name=\'selectedMessages[]\']');\"><i class='fa fa-check' aria-hidden='true'></i></a> <button type='button' class='btn btn-default dropdown-toggle' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'> <span class='caret'></span> <span class='sr-only'>Toggle Dropdown</span> </button> <ul class='dropdown-menu'> <li> <a href='#' data-toggle='modal' data-target='#safeBatchDeleteConfirmationModal' data-callback='getCheckboxesValues' data-callbackArgs=\"input[name='selectedMessages[]']\"><span class='text-danger'>Delete Safely</span></a> </li> <li> <a href='#' data-toggle='modal' data-target='#batchDeleteConfirmationModal' data-callback='getCheckboxesValues' data-callbackArgs=\"input[name='selectedMessages[]']\"><span class='text-danger'>Delete Forever</span></a> </li> </ul> </div>"; $numberHeadCell->setContent($numberHeadCellContent); $data['listHeaders'][0] = $numberHeadCell; } $htmlUtility = new HtmlUtility(); $headRow = $htmlUtility->generateHeadRowByListHeaders($data['listHeaders']); $head = new Head($headRow); $body = new Body(); if (!empty($messages)) { $counter = $this->getCurrentComponentTemplate()->getPager()->getCounterStartNumber(); foreach ($messages as $message) { /** * @var CodeJetter\components\contact\models\ContactMessage $message */ $id = $message->getId(); $name = (new StringUtility())->prepareForView($message->getName()); $email = (new StringUtility())->prepareForView($message->getEmail()); $message = (new StringUtility())->prepareForView($message->getMessage()); $checkbox = $htmlUtility->generateCheckbox('selectedMessages[]', $id); $tmpCell1 = new Cell($checkbox . ' ' . $counter); $tmpCell2 = new Cell($name); $tmpCell3 = new Cell($email); $tmpCell4 = new Cell($message); $cell6Content = "<div class='btn-group'> <button type='button' class='btn btn-default dropdown-toggle' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'> <span class='caret'></span> <span class='sr-only'>Toggle Dropdown</span> </button> <ul class='dropdown-menu'> <li> <a href='#' data-toggle='modal' data-target='#safeDeleteConfirmationModal' data-id='{$id}' data-name='{$name}'><span class='text-danger'>Delete Safely</span></a> </li> <li> <a href='#' data-toggle='modal' data-target='#deleteConfirmationModal' data-id='{$id}' data-name='{$name}'><span class='text-danger'>Delete Forever</span></a> </li> </ul> </div>"; $tmpCell5 = new Cell($cell6Content, false); $tmpRow = new Row([$tmpCell1, $tmpCell2, $tmpCell3, $tmpCell4, $tmpCell5]); $tmpRow->addData('id', $id); $body->addRow($tmpRow); $counter++; } } else { $tmpCell = new Cell('No record.'); $tmpCell->addColspan(5); $body->addRow(new Row([$tmpCell])); } $table = new Table(); $table->class = 'table table-hover'; $table->addHead($head); $table->addBody($body); $tableHtml = $table->getHtml(); $searchFieldHtml = (new HtmlUtility())->generateSearchField($searchQuery, $searchQueryKey); $pagerHtml = $this->getCurrentComponentTemplate()->getPager()->getHtml(); // delete confirmation modal $deleteConfirmationModalHtml = $htmlUtility->generateConfirmationModal('deleteConfirmationModal', 'deleteConfirmationModalLabel', $formHandler, 'deleteForm', '/admin/contact/delete-message'); // batch delete confirmation modal $batchDeleteConfirmationModalHtml = $htmlUtility->generateConfirmationModal('batchDeleteConfirmationModal', 'batchDeleteConfirmationModalLabel', $formHandler, 'deleteForm', '/admin/contact/batch-delete-message'); // safe delete message confirmation modal $safeDeleteModalHtml = $htmlUtility->generateConfirmationModal('safeDeleteConfirmationModal', 'safeDeleteConfirmationModalLabel', $formHandler, 'safeDeleteForm', '/admin/contact/safe-delete-message'); // safe batch delete message confirmation modal $safeBatchDeleteModalHtml = $htmlUtility->generateConfirmationModal('safeBatchDeleteConfirmationModal', 'safeBatchDeleteConfirmationModalLabel', $formHandler, 'safeDeleteForm', '/admin/contact/safe-batch-delete-message'); return "<div class='container-fluid'> <div class='row'> <div class='col-md-offset-1 col-md-10'> {$searchFieldHtml} <div class='row'> <div class='col-lg-12'> {$tableHtml} </div> </div> <div class='row'> <div class='col-lg-12'> {$pagerHtml} </div> </div> </div> </div> </div> <!-- Modals --> {$deleteConfirmationModalHtml} {$batchDeleteConfirmationModalHtml} {$safeDeleteModalHtml} {$safeBatchDeleteModalHtml} <!--/ Modals -->"; |
In this template basically there is a loop that iterates through $messages
. Also PHP Table Generator is used to generate the table which is easy to use. HtmlUtility
is another good tool which is a built-in class and is used for different purposes such as generating confirmation modals or generating the checkboxes in this template.
So far your component directory should be the same as below:
Form Security
In terms of security, there are multiple layers that make the contact form secure. First of all, the form is protected against CSRF attacks thanks to FormHandler
class. Actually this handler generates a random token called globalCSRFToken
which is verified on the server. Also as you saw, all the expected inputs are validated using the Validator
class. In addition, using PDO
the chance of SQL injection attacks is minimum. However, to increase the security you can implement CAPTCHA and client-side validation.
In summary, hope this was helpful to get yourself familiar with the Code Jetter Framework. You can find the code on GitHub and also you are more than welcome to contribute.
Leave a Reply