Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Symfony services in a module

So far all the code sits in the main module class, and it keeps growing. The new PrestaShop core is built on Symfony, and a module can use its service mechanism. A service is an ordinary class whose object is created by the container: it passes everything the class needs into the constructor, for example the database connection.

Autoload

Namespaced classes are kept in the src folder. For PHP to find them without require_once, create a composer.json file in the module root:

{
    "name": "acme/acmehello",
    "type": "prestashop-module",
    "autoload": {
        "psr-4": {
            "Acme\\Hello\\": "src/"
        }
    },
    "config": {
        "prepend-autoloader": false
    }
}

The psr-4 entry links the Acme\Hello namespace with the src folder. Then run a command in the module folder:

composer dump-autoload

A vendor folder with an autoload.php file appears. Include it at the top of the main module file:

// modules/acmehello/acmehello.php
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
    require_once __DIR__ . '/vendor/autoload.php';
}

Structure

modules/acmehello/
├── acmehello.php
├── composer.json
├── config/
│   └── services.yml
├── src/
│   └── Repository/
│       └── ReviewRepository.php
├── vendor/
│   └── autoload.php
└── ...

The service class

Create by hand a class that reads reviews — a repository:

<?php
// modules/acmehello/src/Repository/ReviewRepository.php
namespace Acme\Hello\Repository;
 
use Doctrine\DBAL\Connection;
 
class ReviewRepository
{
    private Connection $connection;
    private string $prefix;
 
    public function __construct(Connection $connection, string $prefix)
    {
        $this->connection = $connection;
        $this->prefix = $prefix;
    }
 
    public function countAll(): int
    {
        return (int) $this->connection->fetchOne(
            'SELECT COUNT(*) FROM ' . $this->prefix . 'acmehello_review'
        );
    }
}

The class does not call Db::getInstance(): it receives the database connection and the table prefix through the constructor. Connection is the connection of the Doctrine DBAL library used by the new core.

Registering the service

Module services are described in the config/services.yml file:

# modules/acmehello/config/services.yml
services:
  acme.hello.review_repository:
    class: Acme\Hello\Repository\ReviewRepository
    public: true
    arguments:
      - '@doctrine.dbal.default_connection'
      - '%database_prefix%'

The first line under services is the service name. arguments lists what to pass to the constructor: an entry with @ is another service, an entry in % signs is a container parameter. public: true allows getting the service by name.

The container is cached, so clear the cache after editing the file:

php bin/console cache:clear

Getting the service

In the main module class a service is obtained with the get() method:

// modules/acmehello/acmehello.php, the getContent method
$total = $this->get('acme.hello.review_repository')->countAll();
 
$output .= $this->displayInformation(
    $this->trans('Reviews in total: %count%', ['%count%' => $total], 'Modules.Acmehello.Admin')
);

Open the module settings page: a blue notice with the number of reviews appears above the form.

You can check that the service is registered from the console:

php bin/console debug:container acme.hello.review_repository

The command prints the class name and the arguments of the service.

Where the file applies

FileWhere the services are available
config/services.ymlIn the back office and in the console
config/admin/services.ymlOnly in the back office
config/front/services.ymlOnly in the front office

The front office container is smaller: it has no back office services. If a service is needed in a front office hook, describe it in config/front/services.yml and use in its arguments only what is available there.

One more method

// modules/acmehello/src/Repository/ReviewRepository.php
public function findLatest(int $limit = 5): array
{
    return $this->connection->fetchAllAssociative(
        'SELECT id_review, author, rating, date_add FROM ' . $this->prefix . 'acmehello_review
         ORDER BY date_add DESC LIMIT ' . (int) $limit
    );
}

The method returns an array of the latest reviews. It is used on the back office page in the next lesson.

Common mistake

A service is added to services.yml, but $this->get() says there is no such service. There are usually two reasons: the container cache is not cleared, or the vendor folder is not created and the class is not found. Run composer dump-autoload in the module folder and php bin/console cache:clear in the shop root.

Thanks for reading!

Donate to the site — we work hard for you.