Using front controllers to create a new page

0
7497
21 min read

In this article, by Fabien Serny, author of PrestaShop Module Development, you will learn about controllers and object models. Controllers handle display on front and permit us to create new page type. Object models handle all required database requests.

We will also see that, sometimes, hooks are not enough and can’t change the way PrestaShop works. In these cases, we will use overrides, which permit us to alter the default process of PrestaShop without making changes in the core code.

If you need to create a complex module, you will need to use front controllers. First of all, using front controllers will permit to split the code in several classes (and files) instead of coding all your module actions in the same class. Also, unlike hooks (that handle some of the display in the existing PrestaShop pages), it will allow you to create new pages.

(For more resources related to this topic, see here.)


Creating the front controller

To make this section easier to understand, we will make an improvement on our current module. Instead of displaying all of the comments (there can be many), we will only display the last three comments and a link that redirects to a page containing all the comments of the product.

First of all, we will add a limit to the Db request in the assignProductTabContent method of your module class that retrieves the comments on the product page:

$comments = Db::getInstance()->executeS('
SELECT * FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` = '.(int)$id_product.'
ORDER BY `date_add` DESC
LIMIT 3');

Now, if you go to a product, you should only see the last three comments.

We will now create a controller that will display all comments concerning a specific product. Go to your module’s root directory and create the following directory path:

/controllers/front/

Create the file that will contain the controller. You have to choose a simple and explicit name since the filename will be used in the URL; let’s name it comments.php. In this file, create a class and name it, ensuring that you follow the [ModuleName][ControllerFilename]ModuleFrontController convention, which extends the ModuleFrontController class.

So in our case, the file will be as follows:

<?php
class MyModCommentsCommentsModuleFrontController extends
ModuleFrontController
{
}

The naming convention has been defined by PrestaShop and must be respected. The class names are a bit long, but they enable us to avoid having two identical class names in different modules.

Now you just to have to set the template file you want to display with the following lines:

class MyModCommentsCommentsModuleFrontController extends
ModuleFrontController
{
public function initContent()
{
parent::initContent();
$this->setTemplate('list.tpl');
}
}

Next, create a template named list.tpl and place it in views/templates/front/ of your module’s directory:

<h1>{l s='Comments' mod='mymodcomments'}</h1>

Now, you can check the result by loading this link on your shop:

/index.php?fc=module&module=mymodcomments&controller=comments

You should see the Comments title displayed.

The fc parameter defines the front controller type, the module parameter defines in which module directory the front controller is, and, at last, the controller parameter defines which controller file to load.

Maintaining compatibility with the Friendly URL option

In order to let the visitor access the controller page we created in the preceding section, we will just add a link between the last three comments displayed and the comment form in the displayProductTabContent.tpl template.

To maintain compatibility with the Friendly URL option of PrestaShop, we will use the getModuleLink method. This will generate a URL according to the URL settings (defined in Preferences | SEO & URLs). If the Friendly URL option is enabled, then it will generate a friendly URL (for example, /en/5-tshirts-doctor-who); if not, it will generate a classic URL (for example, /index.php?id_category=5&controller=category&id_lang=1).

This function takes three parameters: the name of the module, the controller filename you want to call, and an array of parameters. The array of parameters must contain all of the data that’s needed, which will be used by the controller. In our case, we will need at least the product identifier, id_product, to display only the comments related to the product.

We can also add a module_action parameter just in case our controller contains several possible actions.

Here is an example. As you will notice, I created the parameters array directly in the template using the assign Smarty method. From my point of view, it is easier to have the content of the parameters close to the link. However, if you want, you can create this array in your module class and assign it to your template in order to have cleaner code:

<div class="rte">
{assign var=params value=[
'module_action' => 'list',
'id_product'=> $smarty.get.id_product
]}
<a href="{$link->getModuleLink('mymodcomments', 'comments',
$params)}">
{l s='See all comments' mod='mymodcomments'}
</a>
</div>

Now, go to your product page and click on the link; the URL displayed should look something like this:

/index.php?module_action=list&id_product=1&fc=module&module=mymodcomments&controller=comments&id_lang=1

Creating a small action dispatcher

In our case, we won’t need to have several possible actions in the comments controller. However, it would be great to create a small dispatcher in our front controller just in case we want to add other actions later.

To do so, in controllers/front/comments.php, we will create new methods corresponding to each action. I propose to use the init[Action] naming convention (but this is not mandatory). So in our case, it will be a method named initList:

protected function initList()
{
$this->setTemplate('list.tpl');
}

Now in the initContent method, we will create a $actions_list array containing all possible actions and associated callbacks:

$actions_list = array('list' => 'initList');

Now, we will retrieve the id_product and module_action parameters in variables. Once complete, we will check whether the id_product parameter is valid and if the action exists by checking in the $actions_list array. If the method exists, we will dynamically call it:

if ($id_product > 0 && isset($actions_list[$module_action]))
$this->$actions_list[$module_action]();

Here’s what your code should look like:

public function initContent()
{
parent::initContent();
$id_product = (int)Tools::getValue('id_product');
$module_action = Tools::getValue('module_action');
$actions_list = array('list' => 'initList');
if ($id_product > 0 && isset($actions_list[$module_action]))
$this->$actions_list[$module_action]();
}

If you did this correctly nothing should have changed when you refreshed the page on your browser, and the Comments title should still be displayed.

Displaying the product name and comments

We will now display the product name (to let the visitor know he or she is on the right page) and associated comments. First of all, create a public variable, $product, in your controller class, and insert it in the initContent method with an instance of the selected product. This way, the product object will be available in every action method:

$this->product = new Product((int)$id_product, false,
$this->context->cookie->id_lang);

In the initList method, just before setTemplate, we will make a DB request to get all comments associated with the product and then assign the product object and the comments list to Smarty:

// Get comments
$comments = Db::getInstance()->executeS('
SELECT * FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` = '.(int)$this->product->id.'
ORDER BY `date_add` DESC');
// Assign comments and product object
$this->context->smarty->assign('comments', $comments);
$this->context->smarty->assign('product', $this->product);

Once complete, we will display the product name by changing the h1 title:

<h1>
{l s='Comments on product' mod='mymodcomments'}
"{$product->name}"
</h1>

If you refresh your page, you should now see the product name displayed.

I won’t explain this part since it’s exactly the same HTML code we used in the displayProductTabContent.tpl template. At this point, the comments should appear without the CSS style; do not panic, just go to the next section of this article.

Including CSS and JS media in the controller

As you can see, the comments are now displayed. However, you are probably asking yourself why the CSS style hasn’t been applied properly. If you look back at your module class, you will see that it is the hookDisplayProductTab hook in the product page that includes the CSS and JS files. The problem is that we are not on a product page here.

So we have to include them on this page. To do so, we will create a method named setMedia in our controller and add CS and JS files (as we did in the hookDisplayProductTab hook). It will override the default setMedia method contained in the FrontController class. Since this method includes general CSS and JS files used by PrestaShop, it is very important to call the setMedia parent method in our override:

public function setMedia()
{
// We call the parent method
parent::setMedia();
// Save the module path in a variable
$this->path = __PS_BASE_URI__.'modules/mymodcomments/';
// Include the module CSS and JS files needed
$this->context->controller->addCSS($this->path.'views/css/starrating.
css', 'all');
$this->context->controller->addJS($this->path.'views/js/starrating.
js');
$this->context->controller->addCSS($this->path.'views/css
/mymodcomments.css', 'all');
$this->context->controller->addJS($this->path.'views/js
/mymodcomments.js');
}

If you refresh your browser, the comments should now appear well formatted.

In an attempt to improve the display, we will just add the date of the comment beside the author’s name. Just replace <p>{$comment.firstname} {$comment.lastname|substr:0:1}.</p> in your list.tpl template with this line:

<div>{$comment.firstname} {$comment.lastname|substr:0:1}. 
<small>{$comment.date_add|substr:0:10}</small></div>

You can also replace the same line in the displayProductTabContent.tpl template if you want.

If you want more information on how the Smarty method works, such as substr that I used for the date, you can check the official Smarty documentation.

Adding a pagination system

Your controller page is now fully working. However, if one of your products has thousands of comments, the display won’t be quick. We will add a pagination system to handle this case.

First of all, in the initList method, we need to set a number of comments per page and know how many comments are associated with the product:

// Get number of comments
$nb_comments = Db::getInstance()->getValue('
SELECT COUNT(`id_product`)
FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` = '.(int)$this->product->id);
// Init
$nb_per_page = 10;

By default, I have set the number per page to 10, but you can set the number you want. The value is stored in a variable to easily change the number, if needed.

Now we just have to calculate how many pages there will be :

$nb_pages = ceil($nb_comments / $nb_per_page);

Also, set the page the visitor is on:

$page = 1;
if (Tools::getValue('page') != '')
$page = (int)$_GET['page'];

Now that we have this data, we can generate the SQL limit and use it in the comment’s DB request in such a way so as to display the 10 comments corresponding to the page the visitor is on:

$limit_start = ($page - 1) * $nb_per_page;
$limit_end = $nb_per_page;
$comments = Db::getInstance()->executeS('
SELECT * FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` = '.(int)$this->product->id.'
ORDER BY `date_add` DESC
LIMIT '.(int)$limit_start.','.(int)$limit_end);

If you refresh your browser, you should only see the last 10 comments displayed. To conclude, we just need to add links to the different pages for navigation.

First, assign the page the visitor is on and the total number of pages to Smarty:

$this->context->smarty->assign('page', $page);
$this->context->smarty->assign('nb_pages', $nb_pages);

Then in the list.tpl template, we will display numbers in a list from 1 to the total number of pages. On each number, we will add a link with the getModuleLink method we saw earlier, with an additional parameter page:

<ul class="pagination">
{for $count=1 to $nb_pages}
{assign var=params value=[
'module_action' => 'list',
'id_product' => $smarty.get.id_product,
'page' => $count
]}
<li>
<a href="{$link->getModuleLink('mymodcomments', 'comments',
$params)}">
<span>{$count}</span>
</a>
</li>
{/for}
</ul>

To make the pagination clearer for the visitor, we can use the native CSS class to indicate the page the visitor is on:

{if $page ne $count}
<li><a href="{$link->getModuleLink('mymodcomments', 'comments',
$params)}">
<span>{$count}</span>
</a></li>
{else}
<li class="active current">
<span><span>{$count}</span></span>
</li>
{/if}

Your pagination should now be fully working.

Creating routes for a module’s controller

At the beginning of this article, we chose to use the getModuleLink method to keep compatibility with the Friendly URL option of PrestaShop. Let’s enable this option in the SEO & URLs section under Preferences.

Now go to your product page and look at the target of the See all comments link; it should have changed from /index.php?module_action=list&id_product=1&fc=module&module=mymodcomments&controller=comments&id_lang=1 to /en/module/mymodcomments/comments?module_action=list&id_product=1.

The result is nice, but it is not really a Friendly URL yet.

ISO code at the beginning of URLs appears only if you enabled several languages; so if you have only one language enabled, the ISO code will not appear in the URL in your case.

Since PrestaShop 1.5.3, you can create specific routes for your module’s controllers. To do so, you have to attach your module to the ModuleRoutes hook.

In your module’s install method in mymodcomments.php, add the registerHook method for ModuleRoutes:

// Register hooks
if (!$this->registerHook('displayProductTabContent') ||
!$this->registerHook('displayBackOfficeHeader') ||
!$this->registerHook('ModuleRoutes'))
return false;

Don’t forget; you will have to uninstall/install your module if you want it to be attached to this hook. If you don’t want to uninstall your module (because you don’t want to lose all the comments you filled in), you can go to the Positions section under the Modules section of your back office and hook it manually.

Now we have to create the corresponding hook method in the module’s class. This method will return an array with all the routes we want to add.

The array is a bit complex to explain, so let me write an example first:

public function hookModuleRoutes()
{
return array(
'module-mymodcomments-comments' => array(
'controller' => 'comments',
'rule' => 'product-comments{/:module_action}
{/:id_product}/page{/:page}','keywords' => array(
'id_product' => array(
'regexp' => '[d]+',
'param' => 'id_product'),
'page' => array(
'regexp' => '[d]+',
'param' => 'page'),
'module_action' => array(
'regexp' => '[w]+',
'param' => 'module_action'),
),
'params' => array(
'fc' => 'module',
'module' => 'mymodcomments',
'controller' => 'comments'
)
)
);
}

The array can contain several routes. The naming convention for the array key of a route is module-[ModuleName]-[ModuleControllerName]. So in our case, the key will be module-mymodcomments-comments.

In the array, you have to set the following:

  • The controller; in our case, it is comments.
  • The construction of the route (the rule parameter).
    • You can use all the parameters you passed in the getModuleLink method by using the {/:YourParameter} syntax. PrestaShop will automatically add / before each dynamic parameter. In our case, I chose to construct the route this way (but you can change it if you want):
      product-comments{/:module_action}{/:id_product}/page{/:page}
  • The keywords array corresponding to the dynamic parameters.
    • For each dynamic parameter, you have to set Regexp, which will permit to retrieve it from the URL (basically, [d]+ for the integer values and ‘[w]+’ for string values) and the parameter name.
  • The parameters associated with the route.
    • In the case of a module’s front controller, it will always be the same three parameters: the fc parameter set with the fix value module, the module parameter set with the module name, and the controller parameter set with the filename of the module’s controller.

      Very important
      Now PrestaShop is waiting for a page parameter to build the link. To avoid fatal errors, you will have to set the page parameter to 1 in your getModuleLink parameters in the displayProductTabContent.tpl template:

      {assign var=params value=[
      'module_action' => 'list',
      'id_product' => $smarty.get.id_product,
      'page' => 1
      ]}

Once complete, if you go to a product page, the target of the See all comments link should now be:

/en/product-comments/list/1/page/1

It’s really better, but we can improve it a little more by setting the name of the product in the URL.

In the assignProductTabContent method of your module, we will load the product object and assign it to Smarty:

$product = new Product((int)$id_product, false,
$this->context->cookie->id_lang);
$this->context->smarty->assign('product', $product);

This way, in the displayProductTabContent.tpl template, we will be able to add the product’s rewritten link to the parameters of the getModuleLink method: (do not forget to add it in the list.tpl template too!):

{assign var=params value=[
'module_action' => 'list',
'product_rewrite' => $product->link_rewrite,
'id_product' => $smarty.get.id_product,
'page' => 1
]}

We can now update the rule of the route with the product’s link_rewrite variable:

'product-comments{/:module_action}{/:product_rewrite}
{/:id_product}/page{/:page}'

Do not forget to add the product_rewrite string in the keywords array of the route:

'product_rewrite' => array(
'regexp' => '[w-_]+',
'param' => 'product_rewrite'
),

If you refresh your browser, the link should look like this now:

/en/product-comments/list/tshirt-doctor-who/1/page/1

Nice, isn’t it?

Installing overrides with modules

As we saw in the introduction of this article, sometimes hooks are not sufficient to meet the needs of developers; hooks can’t alter the default process of PrestaShop. We could add code to core classes; however, it is not recommended, as all those core changes will be erased when PrestaShop is updated using the autoupgrade module (even a manual upgrade would be difficult). That’s where overrides take the stage.

Creating the override class

Installing new object models and controller overrides on PrestaShop is very easy.

To do so, you have to create an override directory in the root of your module’s directory. Then, you just have to place your override files respecting the path of the original file that you want to override. When you install the module, PrestaShop will automatically move the override to the overrides directory of PrestaShop.

In our case, we will override the getProducts method of the /classes/Search.php class to display the grade and the number of comments on the product list. So we just have to create the Search.php file in /modules/mymodcomments/override/classes/Search.php, and fill it with:

<?php
class Search extends SearchCore
{
public static function find($id_lang, $expr, $page_number = 1,
$page_size = 1, $order_by = 'position', $order_way = 'desc',
$ajax = false, $use_cookie = true, Context $context = null)
{
}
}

In this method, first of all, we will call the parent method to get the products list and return it:

// Call parent method
$find = parent::find($id_lang, $expr, $page_number, $page_size,
$order_by, $order_way, $ajax, $use_cookie, $context);
// Return products
return $find;

We want to display the information (grade and number of comments) to the products list. So, between the find method call and the return statement, we will add some lines of code.

First, we will check whether $find contains products. The find method can return an empty array when no products match the search. In this case, we don’t have to change the way this method works. We also have to check whether the mymodcomments module has been installed (if the override is being used, the module is most likely to be installed, but as I said, it’s just for security):

if (isset($find['result']) && !empty($find['result']) &&
Module::isInstalled('mymodcomments'))
{
}

If we enter these conditions, we will list the product identifier returned by the find parent method:

// List id product
$products = $find['result'];
$id_product_list = array();
foreach ($products as $p)
$id_product_list[] = (int)$p['id_product'];

Next, we will retrieve the grade average and number of comments for the products in the list:

// Get grade average and nb comments for products in list
$grades_comments = Db::getInstance()->executeS('
SELECT `id_product`, AVG(`grade`) as grade_avg,
count(`id_mymod_comment`) as nb_comments
FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` IN ('.implode(',', $id_product_list).')
GROUP BY `id_product`');

Finally, fill in the $products array with the data (grades and comments) corresponding to each product:

// Associate grade and nb comments with product
foreach ($products as $kp => $p)
foreach ($grades_comments as $gc)
if ($gc['id_product'] == $p['id_product'])
{
$products[$kp]['mymodcomments']['grade_avg'] =
round($gc['grade_avg']);
$products[$kp]['mymodcomments']['nb_comments'] =
$gc['nb_comments'];
}
$find['result'] = $products;

Now, as we saw at the beginning of this section, the overrides of the module are installed when you install the module. So you will have to uninstall/install your module.

Once this is done, you can check the override contained in your module; the content of /modules/mymodcomments/override/classes/Search.php should be copied in /override/classes/Search.php.

If an override of the class already exists, PrestaShop will try to merge it by adding the methods you want to override to the existing override class.

Once the override is added by your module, PrestaShop should have regenerated the cache/class_index.php file (which contains the path of every core class and controller), and the path of the Category class should have changed. Open the cache/class_index.php file and search for ‘Search’; the content of this array should now be:

'Search' =>array ( 'path' => 'override/classes
/Search.php','type' => 'class',),

If it’s not the case, it probably means the permissions of this file are wrong and PrestaShop could not regenerate it. To fix this, just delete this file manually and refresh any page of your PrestaShop. The file will be regenerated and the new path will appear.

Since you uninstalled/installed the module, all your comments should have been deleted. So take 2 minutes to fill in one or two comments on a product. Then search for this product. As you must have noticed, nothing has changed. Data is assigned to Smarty, but not used by the template yet.

To avoid deletion of comments each time you uninstall the module, you should comment the loadSQLFile call in the uninstall method of mymodcomments.php. We will uncomment it once we have finished working with the module.

Editing the template file to display grades on products list

In a perfect world, you should avoid using overrides. In this case, we could have used the displayProductListReviews hook, but I just wanted to show you a simple example with an override. Moreover, this hook exists only since PrestaShop 1.6, so it would not work on PrestaShop 1.5.

Now, we will have to edit the product-list.tpl template of the active theme (by default, it is /themes/default-bootstrap/), so the module won’t be a turnkey module anymore. A merchant who will install this module will have to manually edit this template if he wants to have this feature.

In the product-list.tpl template, just after the short description, check if the $product.mymodcomments variable exists (to test if there are comments on the product), and then display the grade average and the number of comments:

{if isset($product.mymodcomments)}
<p>
<b>{l s='Grade:'}</b> {$product.mymodcomments.grade_avg}/5<br
/>
<b>{l s='Number of comments:'}</b>
{$product.mymodcomments.nb_comments}
</p>
{/if}

Here is what the products list should look like now:

PrestaShop Module Development

Creating a new method in a native class

In our case, we have overridden an existing method of a PrestaShop class. But we could have added a method to an existing class. For example, we could have added a method named getComments to the Product class:

<?php
class Product extends ProductCore
{
public function getComments($limit_start, $limit_end = false)
{
$limit = (int)$limit_start;
if ($limit_end)
$limit = (int)$limit_start.','.(int)$limit_end;
$comments = Db::getInstance()->executeS('
SELECT * FROM `'._DB_PREFIX_.'mymod_comment`
WHERE `id_product` = '.(int)$this->id.'
ORDER BY `date_add` DESC
LIMIT '.$limit);
return $comments;
}
}

This way, you could easily access the product comments everywhere in the code just with an instance of a Product class.

Summary

This article taught us about the main design patterns of PrestaShop and explained how to use them to construct a well-organized application.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here