Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

All 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: AdminAcmehelloStats

acmehello_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:clear

A "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.

Call in the controllerWhat 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.

Thanks for reading!

Donate to the site — we work hard for you.