Forms and AJAX in the front office
The reviews page needs a form so that a customer can leave a review. A front controller handles submitted data in the postProcess() method — the engine calls it before initContent(). For requests from JavaScript the controller has separate methods that answer with JSON.
The form in the template
Add a form to the page_content block of the page template:
{* modules/acmehello/views/templates/front/reviews.tpl *}
<form method="post" action="{$formUrl}">
<input type="hidden" name="token" value="{$formToken}">
<label>{l s='Name' d='Modules.Acmehello.Shop'}
<input type="text" name="author" required>
</label>
<label>{l s='Rating' d='Modules.Acmehello.Shop'}
<input type="number" name="rating" min="1" max="5" value="5">
</label>
<label>{l s='Review' d='Modules.Acmehello.Shop'}
<textarea name="content"></textarea>
</label>
<button type="submit" name="submitReview" class="btn btn-primary">
{l s='Send' d='Modules.Acmehello.Shop'}
</button>
</form>The $formUrl and $formToken variables come from the controller. Add them to the assign() call in initContent():
// modules/acmehello/controllers/front/reviews.php
'formUrl' => $this->context->link->getModuleLink('acmehello', 'reviews', ['id_product' => $productId]),
'formToken' => Tools::getToken(false),Tools::getToken(false) returns a token — a string tied to the customer's session. It lets the controller tell that the form was sent from a shop page and not from another site.
Handling the submit
// modules/acmehello/controllers/front/reviews.php
public function postProcess(): void
{
if (!Tools::isSubmit('submitReview')) {
return;
}
if (!$this->isTokenValid()) {
$this->errors[] = $this->trans('The form has expired, please send it again.', [], 'Modules.Acmehello.Shop');
return;
}
$productId = (int) Tools::getValue('id_product');
$author = trim((string) Tools::getValue('author'));
$rating = (int) Tools::getValue('rating');
$content = trim((string) Tools::getValue('content'));
if ($author === '' || !Validate::isGenericName($author)) {
$this->errors[] = $this->trans('Enter your name.', [], 'Modules.Acmehello.Shop');
}
if ($rating < 1 || $rating > 5) {
$this->errors[] = $this->trans('The rating is from 1 to 5.', [], 'Modules.Acmehello.Shop');
}
if (!Validate::isMessage($content)) {
$this->errors[] = $this->trans('The text contains forbidden characters.', [], 'Modules.Acmehello.Shop');
}
if ($this->errors) {
return;
}
$this->module->addReview($productId, $author, $rating, $content);
$this->success[] = $this->trans('Thank you for your review!', [], 'Modules.Acmehello.Shop');
$this->redirectWithNotifications(
$this->context->link->getModuleLink('acmehello', 'reviews', ['id_product' => $productId])
);
}Open the reviews page, fill in the form and send it. The page reloads, a green "Thank you for your review!" message appears at the top, and the new review is in the list. If the name is empty, a red error is shown and the review is not saved.
What happens here
| Call | What for |
|---|---|
Tools::isSubmit('submitReview') | Checks that the form was sent |
isTokenValid() | Compares the token field with the session token |
$this->errors[] | Collects errors; the theme shows them above the content |
$this->success[] | A success message |
redirectWithNotifications() | Redirects and carries the messages over to the next page |
The redirect after a successful submit is needed so that refreshing the page does not send the form again.
An AJAX request
When the request has the ajax=1 parameter, the engine calls, instead of initContent(), a method named displayAjax plus the value of the action parameter:
// modules/acmehello/controllers/front/reviews.php
public function displayAjaxLatest(): void
{
$reviews = $this->module->getReviews((int) Tools::getValue('id_product'));
header('Content-Type: application/json');
$this->ajaxRender(json_encode(['reviews' => $reviews]));
}Open /module/acmehello/reviews?id_product=1&ajax=1&action=latest: the browser shows JSON with the list of reviews, without the header and footer.
Calling it from a script
The address is passed to JavaScript through Media::addJsDef() in the main module class:
// modules/acmehello/acmehello.php, the hookActionFrontControllerSetMedia method
Media::addJsDef([
'acmeHelloReviewsUrl' => $this->context->link->getModuleLink('acmehello', 'reviews', [
'id_product' => (int) Tools::getValue('id_product'),
'ajax' => 1,
'action' => 'latest',
]),
]);// modules/acmehello/views/js/front.js
fetch(acmeHelloReviewsUrl)
.then((response) => response.json())
.then((data) => {
console.log(data.reviews.length);
});On a product page the browser console prints the number of reviews.
Common mistake
A controller accepts a form and does not check the token. Then another site can send it on behalf of a logged-in customer — a request forgery (CSRF). Add a hidden token field to the form and call isTokenValid() at the start of postProcess(). The same applies to AJAX requests that change something.