A back office page on Symfony
New back office pages are written the same way as in an ordinary Symfony project: a route links an address with a controller method, the controller prepares data, and a Twig template draws the page. Let us build a review statistics page this way. This lesson needs the autoload and the service from the previous one.
What we create
modules/acmehello/
├── config/
│ ├── routes.yml
│ └── services.yml
├── src/
│ ├── Controller/
│ │ └── Admin/
│ │ └── StatsController.php
│ └── Repository/
│ └── ReviewRepository.php
└── views/
└── templates/
└── admin/
└── stats.html.twigAll three files are created by hand.
The route
# modules/acmehello/config/routes.yml
acmehello_stats:
path: /acmehello/stats
methods: [GET]
defaults:
_controller: 'Acme\Hello\Controller\Admin\StatsController::indexAction'
_legacy_controller: AdminAcmehelloStats
_legacy_link: AdminAcmehelloStatsacmehello_stats is the route name, path is the address, _controller is the class and method that answers. _legacy_controller links the route with a tab and permissions, and _legacy_link lets you build a link with the familiar getAdminLink('AdminAcmehelloStats').
The engine adds the /modules prefix to module addresses, so the page opens at an address like /admin437xyz/index.php/modules/acmehello/stats.
The controller
<?php
// modules/acmehello/src/Controller/Admin/StatsController.php
namespace Acme\Hello\Controller\Admin;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Symfony\Component\HttpFoundation\Response;
class StatsController extends FrameworkBundleAdminController
{
public function indexAction(): Response
{
return $this->render('@Modules/acmehello/views/templates/admin/stats.html.twig', [
'layoutTitle' => 'Review statistics',
]);
}
}The method returns a response object. render() builds it from a template; the path starts with @Modules and the module name.
The template
{# modules/acmehello/views/templates/admin/stats.html.twig #}
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
<div class="card">
<h3 class="card-header">Reviews</h3>
<div class="card-body">
<p>Statistics will be here.</p>
</div>
</div>
{% endblock %}The template extends the common back office layout: the menu, the header and the page title are added automatically. The content is written in the content block.
The tab
Add the page to the tabs property of the main class and give the route name:
// modules/acmehello/acmehello.php
[
'name' => 'Review statistics',
'class_name' => 'AdminAcmehelloStats',
'route_name' => 'acmehello_stats',
'parent_class_name' => 'AdminCatalog',
'visible' => true,
],Reset the module and clear the cache:
php bin/console prestashop:module reset acmehello
php bin/console cache:clearA "Review statistics" item appears in the "Catalog" section; clicking it opens the page with a card.
Data from the service
// modules/acmehello/src/Controller/Admin/StatsController.php
public function indexAction(): Response
{
$repository = $this->get('acme.hello.review_repository');
return $this->render('@Modules/acmehello/views/templates/admin/stats.html.twig', [
'layoutTitle' => 'Review statistics',
'total' => $repository->countAll(),
'latest' => $repository->findLatest(5),
]);
}{% block content %}
<div class="card">
<h3 class="card-header">Reviews in total: {{ total }}</h3>
<div class="card-body">
<ul>
{% for review in latest %}
<li>{{ review.author }} — {{ review.rating }}/5</li>
{% else %}
<li>No reviews yet.</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}The page shows the total number of reviews and the latest five. Twig escapes variables by itself.
Links and messages
| Call in the controller | What it does |
|---|---|
$this->generateUrl('acmehello_stats') | The route address with the token |
$this->redirectToRoute('acmehello_stats') | A redirect to the route |
$this->addFlash('success', 'Done') | A green message on the next page |
$this->trans('Text', 'Modules.Acmehello.Admin') | Translates a string |
In a template a link is built with the {{ path('acmehello_stats') }} function.
Permissions
The annotation @AdminSecurity("is_granted('read', request.get('_legacy_controller'))") from the PrestaShopBundle\Security\Annotation namespace is placed before a controller method. It checks that the employee profile has the view permission for the tab.
Differences in PrestaShop 9
In version 9 the base class is called PrestaShopAdminController, services are passed into method arguments or the constructor instead of being fetched with $this->get(), and PHP attributes are used instead of annotations. Routes and templates stay the same.
Common mistake
A route is added, but the page returns "No route found". Routes are cached together with the container, and the module must be installed and enabled, otherwise its routes.yml is not read. Clear the cache and check the list with php bin/console debug:router, looking for acmehello_stats in the output.