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-autoloadA 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:clearGetting 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_repositoryThe command prints the class name and the arguments of the service.
Where the file applies
| File | Where the services are available |
|---|---|
config/services.yml | In the back office and in the console |
config/admin/services.yml | Only in the back office |
config/front/services.yml | Only 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.