PrestaShop 9 въвежда значителни промени в архитектурата и стандартите за разработка на модули. Тази статия ще ви въведе в новите особености и ще ви помогне да адаптирате вашия workflow към най-новата версия.

Кратко резюме

Нови изисквания: PHP 8.1+, строго типизиране, Symfony DI
Ключови промени: Нова файлова структура, нови hook-ове
Съвместимост: Как да поддържате PS 8.x паралелно
Практически пример: Готов за употреба модул с код

Ключови промени в архитектурата на модулите

1. Нова структура на файловете

PrestaShop 9 въвежда по-стриктна организация на файловете в модулите:

my_module/
├── config/
│   └── services.yml
├── src/
│   ├── Controller/
│   ├── Entity/
│   ├── Repository/
│   └── Service/
├── templates/
│   ├── admin/
│   └── front/
├── translations/
├── views/
│   ├── css/
│   └── js/
├── my_module.php
└── composer.json

2. Задължителна употреба на Symfony компоненти

Модулите в PS9 трябва да използват Symfony dependency injection контейнера:

// config/services.yml
services:
  my_module.service.product_manager:
    class: MyModule\Service\ProductManager
    arguments:
      - '@doctrine.orm.entity_manager'
      - '@prestashop.core.admin.url_generator'

3. Типизирани свойства и методи

Новите стандарти изискват строго типизиране:

<?php

declare(strict_types=1);

namespace MyModule\Service;

use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;

class ProductManager
{
    public function __construct(
        private EntityManagerInterface $entityManager,
        private AdminUrlGenerator $urlGenerator
    ) {
    }

    public function getProductById(int $productId): ?Product
    {
        return $this->entityManager->getRepository(Product::class)
            ->find(new ProductId($productId));
    }
}

Новите Hook-ове в PrestaShop 9

PrestaShop 9 въвежда нови hook-ове, които предоставят по-добър контрол над функционалността:

Административни Hook-ове

// В главния клас на модула
public function hookActionAdminProductsControllerSaveBefore($params): void
{
    $product = $params['product'];

    // Валидация или модификация преди запазване
    if (!$this->validateCustomFields($product)) {
        throw new PrestaShopException('Невалидни данни');
    }
}

public function hookDisplayAdminProductsMainStepLeftColumnMiddle($params): string
{
    $productId = $params['id_product'];

    $this->context->smarty->assign([
        'custom_data' => $this->getCustomProductData($productId),
        'module_path' => $this->_path,
    ]);

    return $this->display(__FILE__, 'views/templates/admin/product_form.tpl');
}

Front-end Hook-ове

public function hookDisplayProductAdditionalInfo($params): string
{
    $product = $params['product'];

    return $this->display(__FILE__, 'views/templates/hook/product_additional_info.tpl');
}

public function hookActionFrontControllerSetMedia(): void
{
    // Регистриране на CSS и JS файлове
    $this->context->controller->registerStylesheet(
        'module-mystyle',
        'modules/' . $this->name . '/views/css/front.css'
    );

    $this->context->controller->registerJavascript(
        'module-myjs',
        'modules/' . $this->name . '/views/js/front.js'
    );
}

Съвместимост с предишни версии

За да осигурите съвместимост с PS 1.7.x и PS 8.x:

1. Проверка на версията

public function install(): bool
{
    if (version_compare(_PS_VERSION_, '9.0.0', '>=')) {
        return $this->installForPS9();
    }

    return $this->installForLegacy();
}

private function installForPS9(): bool
{
    return parent::install() && 
           $this->registerHook('actionAdminProductsControllerSaveBefore') &&
           $this->registerHook('displayProductAdditionalInfo');
}

private function installForLegacy(): bool
{
    return parent::install() && 
           $this->registerHook('actionProductSave') &&
           $this->registerHook('displayProductTab');
}

2. Условни hook методи

public function hookActionProductSave($params): void
{
    // Логика за PS 1.7.x и PS 8.x
    if (version_compare(_PS_VERSION_, '9.0.0', '<')) {
        $this->handleLegacyProductSave($params);
        return;
    }

    // Логика за PS 9+
    $this->handlePS9ProductSave($params);
}

Примерен модул с пълен код

Ето пример за прост модул, който добавя custom поле към продуктите:

Основен клас (customproductfields.php)

<?php

declare(strict_types=1);

if (!defined('_PS_VERSION_')) {
    exit;
}

require_once __DIR__ . '/vendor/autoload.php';

use CustomProductFields\Service\ProductFieldsManager;

class CustomProductFields extends Module
{
    public function __construct()
    {
        $this->name = 'customproductfields';
        $this->tab = 'administration';
        $this->version = '1.0.0';
        $this->author = 'Presta.bg';
        $this->need_instance = 0;
        $this->ps_versions_compliancy = ['min' => '8.0', 'max' => _PS_VERSION_];

        parent::__construct();

        $this->displayName = $this->l('Custom Product Fields');
        $this->description = $this->l('Добавя персонализирани полета към продуктите');
        $this->confirmUninstall = $this->l('Сигурни ли сте, че искате да деинсталирате модула?');
    }

    public function install(): bool
    {
        return parent::install() &&
               $this->installDatabase() &&
               $this->registerHook('displayAdminProductsMainStepLeftColumnMiddle') &&
               $this->registerHook('actionAdminProductsControllerSaveBefore') &&
               $this->registerHook('displayProductAdditionalInfo');
    }

    public function uninstall(): bool
    {
        return parent::uninstall() && $this->uninstallDatabase();
    }

    private function installDatabase(): bool
    {
        $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'custom_product_fields` (
            `id_product` int(10) unsigned NOT NULL,
            `custom_field_1` text,
            `custom_field_2` text,
            PRIMARY KEY (`id_product`)
        ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';

        return Db::getInstance()->execute($sql);
    }

    private function uninstallDatabase(): bool
    {
        $sql = 'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'custom_product_fields`';

        return Db::getInstance()->execute($sql);
    }

    public function hookDisplayAdminProductsMainStepLeftColumnMiddle($params): string
    {
        $productId = (int) $params['id_product'];
        $customData = $this->getCustomFields($productId);

        $this->context->smarty->assign([
            'custom_field_1' => $customData['custom_field_1'] ?? '',
            'custom_field_2' => $customData['custom_field_2'] ?? '',
        ]);

        return $this->display(__FILE__, 'views/templates/admin/product_form.tpl');
    }

    public function hookActionAdminProductsControllerSaveBefore($params): void
    {
        $productId = (int) $params['id_product'];
        $customField1 = Tools::getValue('custom_field_1');
        $customField2 = Tools::getValue('custom_field_2');

        $this->saveCustomFields($productId, $customField1, $customField2);
    }

    private function getCustomFields(int $productId): array
    {
        $sql = 'SELECT * FROM `' . _DB_PREFIX_ . 'custom_product_fields` 
                WHERE `id_product` = ' . (int) $productId;

        $result = Db::getInstance()->getRow($sql);

        return $result ?: [];
    }

    private function saveCustomFields(int $productId, string $field1, string $field2): bool
    {
        $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'custom_product_fields` 
                (`id_product`, `custom_field_1`, `custom_field_2`) 
                VALUES (' . (int) $productId . ', "' . pSQL($field1) . '", "' . pSQL($field2) . '")
                ON DUPLICATE KEY UPDATE 
                `custom_field_1` = "' . pSQL($field1) . '", 
                `custom_field_2` = "' . pSQL($field2) . '"';

        return Db::getInstance()->execute($sql);
    }
}

Template за администрацията (views/templates/admin/product_form.tpl)

<div class="form-group">
    <label class="control-label col-lg-3">
        {l s='Custom Field 1' mod='customproductfields'}
    </label>
    <div class="col-lg-9">
        <input type="text" 
               name="custom_field_1" 
               value="{$custom_field_1|escape:'html':'UTF-8'}" 
               class="form-control" />
    </div>
</div>

<div class="form-group">
    <label class="control-label col-lg-3">
        {l s='Custom Field 2' mod='customproductfields'}
    </label>
    <div class="col-lg-9">
        <textarea name="custom_field_2" 
                  class="form-control" 
                  rows="3">{$custom_field_2|escape:'html':'UTF-8'}</textarea>
    </div>
</div>

Заключение

PrestaShop 9 носи значителни подобрения в архитектурата на модулите, които правят разработката по-структурирана и модерна. Ключовите промени включват:

  • Задължителна употреба на типизирани методи и свойства
  • Интеграция със Symfony компоненти
  • Нови и по-гъвкави hook-ове
  • По-добра организация на файловете

При разработка на нови модули препоръчваме да следвате новите стандарти, като същевременно поддържате съвместимост с предишни версии за по-широко покритие на пазара.

За по-подробна информация относно специфични API промени, консултирайте се с официалната документация на PrestaShop 9.