Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

CallWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.