Розробка кастомного модуля PrestaShop
Модуль — основна одиниця розширення PrestaShop. Через модулі реалізується будь-яка кастомна логіка: від кастомних полів замовлення до інтеграцій із зовнішніми API. Модуль може реєструвати хуки, додавати контролери, розширювати схему БД, додавати віджети в back-office через Symfony DI.
Структура модуля
modules/mymodule/
├── mymodule.php # Головний клас модуля
├── config.xml # Метадані (опціонально)
├── composer.json # Залежності (опціонально)
├── logo.png # Іконка 32x32
├── controllers/
│ └── front/
│ └── callback.php # FrontController: /module/mymodule/callback
├── src/ # PSR-4 namespace класи
│ ├── Entity/
│ ├── Repository/
│ └── Service/
├── views/
│ └── templates/
│ ├── admin/
│ │ └── configure.html.twig
│ └── hook/
│ └── display_banner.tpl
├── translations/ # Переводи модуля
└── upgrade/ # SQL-міграції при оновленні
└── upgrade-1.1.0.php
Головний клас та життєвий цикл
<?php
declare(strict_types=1);
use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
if (!defined('_PS_VERSION_')) {
exit;
}
class MyModule extends Module
{
public function __construct()
{
$this->name = 'mymodule';
$this->tab = 'other';
$this->version = '1.2.0';
$this->author = 'Company Name';
$this->need_instance = 0;
$this->bootstrap = true;
$this->ps_versions_compliancy = ['min' => '8.0.0', 'max' => _PS_VERSION_];
parent::__construct();
$this->displayName = $this->trans('My Module', [], 'Modules.Mymodule.Admin');
$this->description = $this->trans('Module description', [], 'Modules.Mymodule.Admin');
}
public function install(): bool
{
return parent::install()
&& $this->installDb()
&& $this->registerHook('actionOrderStatusPostUpdate')
&& $this->registerHook('displayCustomerAccount')
&& $this->registerHook('displayHeader');
}
public function uninstall(): bool
{
return parent::uninstall() && $this->uninstallDb();
}
private function installDb(): bool
{
$sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mymodule_data` (
`id_mymodule` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_order` INT UNSIGNED NOT NULL,
`payload` TEXT,
`status` TINYINT(1) NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL,
PRIMARY KEY (`id_mymodule`),
KEY `id_order` (`id_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;';
return Db::getInstance()->execute($sql);
}
}
Робота з хуками
Хуки бувають двох типів: action (змінюють поведінку, повертають дані) та display (повертають HTML). Важливо розрізняти:
// Display hook — повертає HTML-рядок
public function hookDisplayCustomerAccount(array $params): string
{
$customerId = (int) $this->context->customer->id;
$data = $this->getCustomerData($customerId);
if (empty($data)) {
return '';
}
$this->smarty->assign([
'mymodule_data' => $data,
'moduleUrl' => $this->context->link->getModuleLink('mymodule', 'account'),
]);
return $this->display(__FILE__, 'views/templates/hook/customer_account.tpl');
}
// Action hook — модифікує дані або виконує side-effect
public function hookActionOrderStatusPostUpdate(array $params): void
{
/** @var Order $order */
$order = $params['order'];
$newStatus = $params['newOrderStatus'];
if ((int) $newStatus->id === (int) Configuration::get('PS_OS_PAYMENT')) {
$this->notifyExternalSystem($order);
}
}
Symfony DI та сучасний back-office
PrestaShop 8.x дозволяє використовувати Symfony DI в модулях через config/services.yml:
# modules/mymodule/config/services.yml
services:
_defaults:
autowire: true
autoconfigure: true
public: true
MyModule\Repository\OrderDataRepository:
arguments:
$connection: '@doctrine.dbal.default_connection'
MyModule\Service\ExternalApiService:
arguments:
$apiKey: '%env(MYMODULE_API_KEY)%'
$logger: '@logger'
Використання у контролері back-office (Symfony-style):
// src/Controller/Admin/OrderDataController.php
namespace MyModule\Controller\Admin;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Symfony\Component\HttpFoundation\JsonResponse;
use MyModule\Repository\OrderDataRepository;
class OrderDataController extends FrameworkBundleAdminController
{
public function __construct(
private readonly OrderDataRepository $repository
) {}
public function indexAction(int $orderId): JsonResponse
{
$data = $this->repository->findByOrderId($orderId);
return $this->json([
'success' => true,
'data' => $data,
]);
}
}
Міграції схеми БД
При оновленні модуля використовуйте upgrade-скрипти:
// modules/mymodule/upgrade/upgrade-1.2.0.php
function upgrade_module_1_2_0(Module $module): bool
{
$result = Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'mymodule_data`
ADD COLUMN `external_id` VARCHAR(128) NULL AFTER `id_order`,
ADD INDEX `external_id` (`external_id`)'
);
if ($result) {
// Міграція даних
Db::getInstance()->execute(
'UPDATE `' . _DB_PREFIX_ . 'mymodule_data` SET `status` = 1 WHERE `status` = 0 AND `date_add` < NOW()'
);
}
return (bool) $result;
}
Локалізація модуля
PrestaShop використовує Symfony Translation для back-office та власний механізм для front-office:
// Back-office (Symfony Translation)
$this->trans('Order processed', [], 'Modules.Mymodule.Admin');
// Front-office (Smarty)
// У .tpl файлі:
// {l s='Order processed' mod='mymodule'}
// Генерація файлів переводів
php bin/console prestashop:generate:translations --module=mymodule
Тестування модуля
// tests/Unit/Service/ExternalApiServiceTest.php
use PHPUnit\Framework\TestCase;
use MyModule\Service\ExternalApiService;
class ExternalApiServiceTest extends TestCase
{
public function testNotifyOrder(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->expects($this->once())
->method('request')
->with('POST', 'https://api.example.com/orders')
->willReturn(new MockResponse('{"status":"ok"}', ['http_code' => 200]));
$service = new ExternalApiService($httpClient, 'test-api-key');
$result = $service->notifyOrderPaid(12345);
$this->assertTrue($result);
}
}
Терміни розробки
- Простий модуль: хуки, конфігурація, вивід даних: 2–4 дні
- Інтеграція із зовнішнім API (платіжна система, CRM): 5–10 днів
- Модуль із кастомними сутностями, CRUD у back-office, міграціями: 7–14 днів
- Складний модуль (кастомний checkout, програма лояльності): 3–6 тижнів







