Files
advancedmegamenu/advancedmegamenu.php
T
tiamak 8e6b6d0cdd
Build / Build & Release draft (push) Failing after 0s
PHP tests / PHP Syntax check 5.6 => 8.1 (push) Failing after 33s
PHP tests / PHPStan (1.7.1.2) (push) Has been cancelled
PHP tests / PHPStan (1.7.2.5) (push) Has been cancelled
PHP tests / PHPStan (1.7.3.4) (push) Has been cancelled
PHP tests / PHPStan (1.7.4.4) (push) Has been cancelled
PHP tests / PHPStan (1.7.5.1) (push) Has been cancelled
PHP tests / PHPStan (1.7.6) (push) Has been cancelled
PHP tests / PHPStan (1.7.7) (push) Has been cancelled
PHP tests / PHPStan (1.7.8) (push) Has been cancelled
PHP tests / PHPStan (latest) (push) Has been cancelled
PHP tests / PHP-CS-Fixer (push) Has been cancelled
Build advanced mega menu module
2026-04-09 13:29:59 +00:00

919 lines
32 KiB
PHP

<?php
if (!defined('_PS_VERSION_')) {
exit;
}
require_once __DIR__ . '/src/autoload.php';
use AdvancedMegaMenu\Classes\SpriteGenerator;
use AdvancedMegaMenu\Model\AdvMenuNode;
use AdvancedMegaMenu\Repository\MenuRepository;
use PrestaShop\PrestaShop\Core\Module\WidgetInterface;
class AdvancedMegaMenu extends Module implements WidgetInterface
{
private const MENU_JSON_CACHE_KEY = 'ADVANCEDMEGAMENU_MENU_JSON';
private const CACHE_DIR_NAME = 'advancedmegamenu';
private const ALLOWED_TYPES = ['category', 'cms', 'link', 'rich_content'];
/** @var array<int, int> */
protected $user_groups = [];
public function __construct()
{
$this->name = 'advancedmegamenu';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Tiamak';
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->trans('Advanced Mega Menu', [], 'Modules.Advancedmegamenu.Admin');
$this->description = $this->trans('Rich mega menu with desktop panels, mobile drill-down navigation and optimized assets.', [], 'Modules.Advancedmegamenu.Admin');
$this->ps_versions_compliancy = ['min' => '1.7.8.0', 'max' => _PS_VERSION_];
}
public function install()
{
return parent::install()
&& $this->registerHook('displayTop')
&& $this->registerHook('displayHeader')
&& $this->registerHook('actionObjectCategoryUpdateAfter')
&& $this->registerHook('actionObjectCategoryDeleteAfter')
&& $this->registerHook('actionObjectCategoryAddAfter')
&& $this->registerHook('actionObjectCmsUpdateAfter')
&& $this->registerHook('actionObjectCmsDeleteAfter')
&& $this->registerHook('actionObjectCmsAddAfter')
&& $this->registerHook('actionObjectProductUpdateAfter')
&& $this->registerHook('actionObjectProductDeleteAfter')
&& $this->registerHook('actionObjectProductAddAfter')
&& $this->registerHook('actionCategoryUpdate')
&& $this->registerHook('actionMetaPageSave')
&& $this->registerHook('actionShopDataDuplication')
&& $this->installDb();
}
public function uninstall()
{
$this->clearMenuCache();
return parent::uninstall() && $this->uninstallDb();
}
public function installDb()
{
$queries = [
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item` (
`id_advmegamenu_item` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_shop` INT UNSIGNED NOT NULL,
`parent_id` INT UNSIGNED NOT NULL DEFAULT 0,
`position` INT UNSIGNED NOT NULL DEFAULT 0,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`type` VARCHAR(32) NOT NULL,
`entity_id` INT UNSIGNED NOT NULL DEFAULT 0,
`icon_path` VARCHAR(255) NOT NULL DEFAULT "",
`new_window` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id_advmegamenu_item`),
KEY `advmegamenu_item_shop` (`id_shop`, `parent_id`, `position`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item_lang` (
`id_advmegamenu_item` INT UNSIGNED NOT NULL,
`id_lang` INT UNSIGNED NOT NULL,
`title` VARCHAR(255) NOT NULL DEFAULT "",
`description` TEXT NULL,
`custom_link` VARCHAR(255) NOT NULL DEFAULT "",
PRIMARY KEY (`id_advmegamenu_item`, `id_lang`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_layout` (
`id_advmegamenu_layout` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_advmegamenu_item` INT UNSIGNED NOT NULL,
`position` INT UNSIGNED NOT NULL DEFAULT 0,
`column_width` TINYINT UNSIGNED NOT NULL DEFAULT 4,
`show_title` TINYINT(1) NOT NULL DEFAULT 1,
`custom_image` VARCHAR(255) NOT NULL DEFAULT "",
`background_color` VARCHAR(32) NOT NULL DEFAULT "",
`block_type` VARCHAR(32) NOT NULL DEFAULT "promo",
PRIMARY KEY (`id_advmegamenu_layout`),
KEY `advmegamenu_layout_item` (`id_advmegamenu_item`, `position`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'advmegamenu_products` (
`id_advmegamenu_products` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id_advmegamenu_layout` INT UNSIGNED NOT NULL,
`id_product` INT UNSIGNED NOT NULL,
`position` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id_advmegamenu_products`),
KEY `advmegamenu_products_layout` (`id_advmegamenu_layout`, `position`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8mb4;',
'INSERT IGNORE INTO `' . _DB_PREFIX_ . 'hook` (`name`, `title`, `description`) VALUES
("actionMainMenuModifier", "Modify advanced mega menu view data", "Allows modules to alter advanced mega menu data");',
];
foreach ($queries as $query) {
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
protected function uninstallDb()
{
$queries = [
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_products`',
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_layout`',
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item_lang`',
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'advmegamenu_item`',
];
foreach ($queries as $query) {
Db::getInstance()->execute($query);
}
return true;
}
public function getContent()
{
if (Tools::getValue('ajax') && Tools::getValue('action')) {
$this->ajaxRouter();
}
$output = $this->postProcess();
$this->context->controller->addCSS($this->_path . 'views/css/admin.css');
$this->context->controller->addJS($this->_path . 'views/js/admin.js');
$languages = $this->context->controller->getLanguages();
$repository = new MenuRepository(Db::getInstance(), $this);
$tree = $repository->getAdminTree((int) $this->context->shop->id, $languages);
$this->context->smarty->assign([
'advmegamenu_languages' => $languages,
'advmegamenu_default_lang' => (int) Configuration::get('PS_LANG_DEFAULT'),
'advmegamenu_tree' => $tree,
'advmegamenu_ajax_url' => $this->getAdminAjaxUrl(),
'advmegamenu_admin_token' => Tools::getAdminTokenLite('AdminModules'),
]);
return $output . $this->display(__FILE__, 'views/templates/admin/configure.tpl');
}
protected function postProcess()
{
if (!Tools::isSubmit('submitAdvancedMegaMenu')) {
return '';
}
$rawTree = Tools::getValue('advmegamenu_tree_json', '[]');
$tree = json_decode($rawTree, true);
if (!is_array($tree)) {
return $this->displayError($this->trans('Invalid menu payload.', [], 'Modules.Advancedmegamenu.Admin'));
}
try {
$this->saveMenuTree($tree);
$this->generateSpriteAssets((int) $this->context->shop->id);
$this->clearMenuCache();
return $this->displayConfirmation($this->trans('The mega menu configuration has been saved.', [], 'Modules.Advancedmegamenu.Admin'));
} catch (Exception $exception) {
return $this->displayError($exception->getMessage());
}
}
public function hookDisplayTop($params)
{
return $this->renderWidget('displayTop', $params);
}
public function hookDisplayHeader()
{
$controller = $this->context->controller;
if (!$controller) {
return;
}
$controller->registerStylesheet(
'module-advancedmegamenu-front',
$this->_path . 'views/css/megamenu.css',
['media' => 'all', 'priority' => 150]
);
$controller->registerJavascript(
'module-advancedmegamenu-front',
$this->_path . 'views/js/megamenu.js',
['position' => 'bottom', 'priority' => 150]
);
$spriteCssFile = $this->getLocalPath() . 'views/css/generated/menu-sprite.css';
if (is_file($spriteCssFile)) {
$controller->registerStylesheet(
'module-advancedmegamenu-sprite',
$this->_path . 'views/css/generated/menu-sprite.css',
['media' => 'all', 'priority' => 151]
);
}
}
public function getWidgetVariables($hookName, array $configuration)
{
$idLang = (int) $this->context->language->id;
$idShop = (int) $this->context->shop->id;
$this->user_groups = Customer::getGroupsStatic((int) $this->context->customer->id);
$groupsKey = empty($this->user_groups) ? '' : '_' . implode('_', $this->user_groups);
$cacheFile = $this->getCacheDirectory() . DIRECTORY_SEPARATOR . self::MENU_JSON_CACHE_KEY . '_' . $idLang . '_' . $idShop . $groupsKey . '.json';
$menu = json_decode((string) @file_get_contents($cacheFile), true);
if (!is_array($menu)) {
$repository = new MenuRepository(Db::getInstance(), $this);
$rawTree = $repository->getRawTree($idShop, $idLang);
$menu = [
'children' => array_map(function (array $node): array {
return $this->buildFrontNode($node);
}, $rawTree),
];
file_put_contents($cacheFile, json_encode($menu));
}
$pageIdentifier = $this->getCurrentPageIdentifier();
return $this->mapTree(function (array $node) use ($pageIdentifier): array {
$node['current'] = ($pageIdentifier === ($node['page_identifier'] ?? null));
return $node;
}, $menu);
}
public function renderWidget($hookName, array $configuration)
{
$menu = $this->getWidgetVariables($hookName, $configuration);
Hook::exec('actionMainMenuModifier', ['menu' => &$menu]);
$this->smarty->assign([
'menu' => $menu,
]);
return $this->display(__FILE__, 'views/templates/hook/megamenu.tpl');
}
/**
* @param array<int, array<string, mixed>> $tree
*/
private function saveMenuTree(array $tree): void
{
$idShop = (int) $this->context->shop->id;
$languages = Language::getLanguages(false);
Db::getInstance()->execute('START TRANSACTION');
try {
$this->deleteMenuTreeForShop($idShop);
foreach (array_values($tree) as $position => $node) {
$this->insertNodeRecursive($node, 0, $position, $idShop, $languages);
}
Db::getInstance()->execute('COMMIT');
} catch (Exception $exception) {
Db::getInstance()->execute('ROLLBACK');
throw $exception;
}
}
private function deleteMenuTreeForShop(int $idShop): void
{
$itemIds = Db::getInstance()->executeS(
'SELECT `id_advmegamenu_item` FROM `' . _DB_PREFIX_ . 'advmegamenu_item` WHERE `id_shop` = ' . (int) $idShop
);
$itemIds = array_map(static function (array $row): int {
return (int) $row['id_advmegamenu_item'];
}, $itemIds ?: []);
if (!empty($itemIds)) {
$layoutIds = Db::getInstance()->executeS(
'SELECT `id_advmegamenu_layout` FROM `' . _DB_PREFIX_ . 'advmegamenu_layout`
WHERE `id_advmegamenu_item` IN (' . implode(',', array_map('intval', $itemIds)) . ')'
);
$layoutIds = array_map(static function (array $row): int {
return (int) $row['id_advmegamenu_layout'];
}, $layoutIds ?: []);
if (!empty($layoutIds)) {
Db::getInstance()->delete('advmegamenu_products', 'id_advmegamenu_layout IN (' . implode(',', array_map('intval', $layoutIds)) . ')');
}
Db::getInstance()->delete('advmegamenu_layout', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
Db::getInstance()->delete('advmegamenu_item_lang', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
Db::getInstance()->delete('advmegamenu_item', 'id_advmegamenu_item IN (' . implode(',', array_map('intval', $itemIds)) . ')');
}
}
/**
* @param array<string, mixed> $node
* @param array<int, array{id_lang:int}> $languages
*/
private function insertNodeRecursive(array $node, int $parentId, int $position, int $idShop, array $languages): void
{
$type = in_array($node['type'] ?? '', self::ALLOWED_TYPES, true) ? $node['type'] : 'link';
$item = new AdvMenuNode();
$item->id_shop = $idShop;
$item->parent_id = $parentId;
$item->position = $position;
$item->active = !empty($node['active']);
$item->type = $type;
$item->entity_id = max(0, (int) ($node['entity_id'] ?? 0));
$item->icon_path = (string) ($node['icon_path'] ?? '');
$item->new_window = !empty($node['new_window']);
if (!$item->add()) {
throw new Exception($this->trans('Unable to save menu item.', [], 'Modules.Advancedmegamenu.Admin'));
}
foreach ($languages as $language) {
$idLang = (int) $language['id_lang'];
$langData = $node['lang'][$idLang] ?? [];
Db::getInstance()->insert('advmegamenu_item_lang', [
'id_advmegamenu_item' => (int) $item->id,
'id_lang' => $idLang,
'title' => pSQL((string) ($langData['title'] ?? ''), true),
'description' => pSQL((string) ($langData['description'] ?? ''), true),
'custom_link' => pSQL((string) ($langData['custom_link'] ?? ''), true),
]);
}
foreach (array_values($node['layouts'] ?? []) as $layoutPosition => $layout) {
Db::getInstance()->insert('advmegamenu_layout', [
'id_advmegamenu_item' => (int) $item->id,
'position' => (int) $layoutPosition,
'column_width' => min(12, max(1, (int) ($layout['column_width'] ?? 4))),
'show_title' => !empty($layout['show_title']),
'custom_image' => pSQL((string) ($layout['custom_image'] ?? '')),
'background_color' => pSQL((string) ($layout['background_color'] ?? '')),
'block_type' => pSQL((string) ($layout['block_type'] ?? 'promo')),
]);
$layoutId = (int) Db::getInstance()->Insert_ID();
foreach (array_values($layout['products'] ?? []) as $productPosition => $idProduct) {
Db::getInstance()->insert('advmegamenu_products', [
'id_advmegamenu_layout' => $layoutId,
'id_product' => (int) $idProduct,
'position' => (int) $productPosition,
]);
}
}
foreach (array_values($node['children'] ?? []) as $childPosition => $child) {
$this->insertNodeRecursive($child, (int) $item->id, $childPosition, $idShop, $languages);
}
}
private function ajaxRouter(): void
{
$action = (string) Tools::getValue('action');
if ($action === 'searchProducts') {
$this->ajaxSearchProducts();
}
if ($action === 'uploadImage') {
$this->ajaxUploadImage();
}
header('Content-Type: application/json');
die(json_encode(['error' => true, 'message' => 'Unknown action']));
}
private function ajaxSearchProducts(): void
{
$term = pSQL((string) Tools::getValue('q'));
$idLang = (int) $this->context->language->id;
$idShop = (int) $this->context->shop->id;
$rows = Db::getInstance()->executeS(
'SELECT p.`id_product`, pl.`name`, p.`reference`
FROM `' . _DB_PREFIX_ . 'product` p
INNER JOIN `' . _DB_PREFIX_ . 'product_shop` ps ON (ps.`id_product` = p.`id_product` AND ps.`id_shop` = ' . $idShop . ')
INNER JOIN `' . _DB_PREFIX_ . 'product_lang` pl ON (
pl.`id_product` = p.`id_product`
AND pl.`id_lang` = ' . $idLang . '
AND pl.`id_shop` = ' . $idShop . '
)
WHERE pl.`name` LIKE "%' . $term . '%" OR p.`reference` LIKE "%' . $term . '%" OR p.`id_product` = "' . (int) $term . '"
ORDER BY pl.`name` ASC
LIMIT 20'
) ?: [];
$results = array_map(static function (array $row): array {
return [
'id' => (int) $row['id_product'],
'label' => trim(sprintf('#%d %s%s', $row['id_product'], $row['name'], $row['reference'] ? ' (' . $row['reference'] . ')' : '')),
];
}, $rows);
header('Content-Type: application/json');
die(json_encode(['results' => $results]));
}
private function ajaxUploadImage(): void
{
if (empty($_FILES['image']['tmp_name'])) {
header('Content-Type: application/json');
die(json_encode(['error' => true, 'message' => 'Missing file']));
}
$preset = (string) Tools::getValue('preset', 'default');
$storedPath = $this->storeUploadedImage($_FILES['image'], $preset);
header('Content-Type: application/json');
die(json_encode([
'error' => false,
'path' => $storedPath,
'url' => $this->buildModuleImageUrl($storedPath),
]));
}
/**
* @param array<string, mixed> $file
*/
private function storeUploadedImage(array $file, string $preset = 'default'): string
{
$uploadDir = $this->getLocalPath() . 'views/img/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$extension = Tools::strtolower(pathinfo((string) $file['name'], PATHINFO_EXTENSION));
$basename = sha1(uniqid((string) mt_rand(), true));
$targetPath = $uploadDir . $basename . '.webp';
$originalPath = $uploadDir . $basename . ($extension ? '.' . $extension : '.bin');
if (!move_uploaded_file((string) $file['tmp_name'], $originalPath)) {
throw new Exception($this->trans('Unable to store uploaded file.', [], 'Modules.Advancedmegamenu.Admin'));
}
$converted = false;
if ($preset === 'menu_icon') {
$converted = $this->convertImageToSquareWebp($originalPath, $targetPath, 250);
}
if (!$converted) {
$converted = $this->convertImageToWebp($originalPath, $targetPath);
}
if ($converted) {
unlink($originalPath);
return 'uploads/' . basename($targetPath);
}
return 'uploads/' . basename($originalPath);
}
private function convertImageToWebp(string $sourcePath, string $targetPath): bool
{
$contents = @file_get_contents($sourcePath);
if ($contents === false) {
return false;
}
$image = @imagecreatefromstring($contents);
if (!$image) {
return false;
}
imagepalettetotruecolor($image);
imagealphablending($image, true);
imagesavealpha($image, true);
$result = imagewebp($image, $targetPath, 85);
imagedestroy($image);
return $result && is_file($targetPath);
}
private function convertImageToSquareWebp(string $sourcePath, string $targetPath, int $size): bool
{
$contents = @file_get_contents($sourcePath);
if ($contents === false) {
return false;
}
$sourceImage = @imagecreatefromstring($contents);
if (!$sourceImage) {
return false;
}
$sourceWidth = imagesx($sourceImage);
$sourceHeight = imagesy($sourceImage);
if ($sourceWidth <= 0 || $sourceHeight <= 0) {
imagedestroy($sourceImage);
return false;
}
$scale = $size / max($sourceWidth, $sourceHeight);
$targetWidth = max(1, (int) round($sourceWidth * $scale));
$targetHeight = max(1, (int) round($sourceHeight * $scale));
$destinationX = (int) floor(($size - $targetWidth) / 2);
$destinationY = (int) floor(($size - $targetHeight) / 2);
$canvas = imagecreatetruecolor($size, $size);
if (!$canvas) {
imagedestroy($sourceImage);
return false;
}
$white = imagecolorallocate($canvas, 255, 255, 255);
imagefill($canvas, 0, 0, $white);
imagealphablending($canvas, true);
imagesavealpha($canvas, false);
$result = imagecopyresampled(
$canvas,
$sourceImage,
$destinationX,
$destinationY,
0,
0,
$targetWidth,
$targetHeight,
$sourceWidth,
$sourceHeight
) && imagewebp($canvas, $targetPath, 85);
imagedestroy($canvas);
imagedestroy($sourceImage);
return $result && is_file($targetPath);
}
private function generateSpriteAssets(int $idShop): void
{
$icons = Db::getInstance()->executeS(
'SELECT `id_advmegamenu_item` AS `id`, `icon_path`
FROM `' . _DB_PREFIX_ . 'advmegamenu_item`
WHERE `id_shop` = ' . (int) $idShop . '
AND `active` = 1
AND `icon_path` != ""'
) ?: [];
$generator = new SpriteGenerator($this->getLocalPath(), $this->getPathUri());
$generator->generate(array_map(static function (array $row): array {
return [
'id' => (int) $row['id'],
'icon_path' => (string) $row['icon_path'],
];
}, $icons));
}
/**
* @param array<string, mixed> $node
*
* @return array<string, mixed>
*/
private function buildFrontNode(array $node): array
{
$title = (string) ($node['title'] ?? '');
$description = (string) ($node['description'] ?? '');
$entityId = (int) ($node['entity_id'] ?? 0);
$itemId = (int) ($node['id'] ?? 0);
$type = (string) ($node['type'] ?? 'link');
$frontNode = [
'id' => $itemId,
'title' => $title,
'description' => $description,
'url' => $this->resolveNodeUrl($node),
'custom_link' => (string) ($node['custom_link'] ?? ''),
'type' => $type,
'active' => !empty($node['active']),
'current' => false,
'new_window' => !empty($node['new_window']),
'page_identifier' => $this->resolvePageIdentifier($type, $entityId, $node),
'icon_class' => $node['icon_path'] ? 'adv-megamenu__icon adv-icon-item-' . $itemId : '',
'icon_url' => (string) ($node['icon_url'] ?? ''),
'children' => array_values(array_filter(array_map(function (array $child): array {
return $this->buildFrontNode($child);
}, $node['children'] ?? []), static function (array $child): bool {
return $child['active'];
})),
'layouts' => $this->buildLayouts($node),
'category_branch' => $type === 'category' && $entityId > 0 ? $this->buildCategoryBranch($entityId) : [],
];
return $frontNode;
}
/**
* @param array<string, mixed> $node
*
* @return array<int, array<string, mixed>>
*/
private function buildLayouts(array $node): array
{
$title = (string) ($node['title'] ?? '');
$description = (string) ($node['description'] ?? '');
return array_map(function (array $layout) use ($title, $description): array {
return [
'id' => (int) $layout['id'],
'column_width' => (int) $layout['column_width'],
'show_title' => (bool) $layout['show_title'],
'custom_image_url' => (string) ($layout['custom_image_url'] ?? ''),
'background_color' => (string) ($layout['background_color'] ?? ''),
'block_type' => (string) ($layout['block_type'] ?? 'promo'),
'title' => $title,
'description' => $description,
'products' => $this->presentProducts($layout['products'] ?? []),
];
}, $node['layouts'] ?? []);
}
/**
* @param array<int, int> $productIds
*
* @return array<int, array<string, mixed>>
*/
private function presentProducts(array $productIds): array
{
if (empty($productIds)) {
return [];
}
$assembler = new ProductAssembler($this->context);
$presenterFactory = new ProductPresenterFactory($this->context);
$presentationSettings = $presenterFactory->getPresentationSettings();
$presenter = $presenterFactory->getPresenter();
$products = [];
foreach ($productIds as $productId) {
$product = new Product((int) $productId, false, (int) $this->context->language->id, (int) $this->context->shop->id);
if (!Validate::isLoadedObject($product) || !$product->active) {
continue;
}
$products[] = $presenter->present(
$presentationSettings,
$assembler->assembleProduct(['id_product' => (int) $productId]),
$this->context->language
);
}
return $products;
}
/**
* @return array<int, array<string, mixed>>
*/
private function buildCategoryBranch(int $categoryId): array
{
$categories = Category::getNestedCategories($categoryId, (int) $this->context->language->id, false, $this->user_groups);
if (!isset($categories[$categoryId]['children']) || !is_array($categories[$categoryId]['children'])) {
return [];
}
return $this->buildCategoryTreeNodes($categories[$categoryId]['children']);
}
/**
* @param array<int, array<string, mixed>> $categories
*
* @return array<int, array<string, mixed>>
*/
private function buildCategoryTreeNodes(array $categories): array
{
$nodes = [];
foreach ($categories as $category) {
if (empty($category['active'])) {
continue;
}
$nodes[] = [
'id' => (int) $category['id_category'],
'title' => (string) $category['name'],
'url' => $this->context->link->getCategoryLink((int) $category['id_category']),
'children' => !empty($category['children']) ? $this->buildCategoryTreeNodes($category['children']) : [],
];
}
return $nodes;
}
/**
* @param array<string, mixed> $node
*/
private function resolveNodeUrl(array $node): string
{
$type = (string) ($node['type'] ?? 'link');
$entityId = (int) ($node['entity_id'] ?? 0);
$customLink = (string) ($node['custom_link'] ?? '');
switch ($type) {
case 'category':
return $entityId > 0 ? $this->context->link->getCategoryLink($entityId) : '#';
case 'cms':
if ($entityId <= 0) {
return '#';
}
return $this->context->link->getCMSLink(new CMS($entityId, (int) $this->context->language->id));
case 'rich_content':
case 'link':
default:
return $customLink !== '' ? $customLink : '#';
}
}
/**
* @param array<string, mixed> $node
*/
private function resolvePageIdentifier(string $type, int $entityId, array $node): string
{
switch ($type) {
case 'category':
return 'category-' . $entityId;
case 'cms':
return 'cms-page-' . $entityId;
case 'link':
case 'rich_content':
default:
return 'adv-link-' . md5((string) ($node['custom_link'] ?? '') . (string) ($node['title'] ?? ''));
}
}
private function getCurrentPageIdentifier(): string
{
$controllerName = Dispatcher::getInstance()->getController();
if ($controllerName === 'cms' && ($id = (int) Tools::getValue('id_cms'))) {
return 'cms-page-' . $id;
}
if ($controllerName === 'category' && ($id = (int) Tools::getValue('id_category'))) {
return 'category-' . $id;
}
return 'route:' . $controllerName;
}
/**
* @param callable $callback
* @param array<string, mixed> $node
*
* @return array<string, mixed>
*/
private function mapTree(callable $callback, array $node): array
{
$node['children'] = array_map(function (array $child) use ($callback): array {
return $this->mapTree($callback, $child);
}, $node['children'] ?? []);
return $callback($node);
}
private function getCacheDirectory(): string
{
$dir = _PS_CACHE_DIR_ . self::CACHE_DIR_NAME;
if (isset($this->context->customer)) {
$groups = $this->context->customer->getGroups();
if (!empty($groups)) {
$dir .= '/' . implode('_', $groups);
}
}
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
return $dir;
}
protected function clearMenuCache()
{
$this->cleanMenuCacheDirectory(_PS_CACHE_DIR_ . self::CACHE_DIR_NAME);
}
private function cleanMenuCacheDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
foreach (scandir($dir) as $entry) {
if (in_array($entry, ['.', '..'], true)) {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
$this->cleanMenuCacheDirectory($path);
continue;
}
if (preg_match('/\.json$/', $entry)) {
unlink($path);
}
}
}
public function hookActionObjectCategoryAddAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectCategoryUpdateAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectCategoryDeleteAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectCmsUpdateAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectCmsDeleteAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectCmsAddAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectProductUpdateAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectProductDeleteAfter($params)
{
$this->clearMenuCache();
}
public function hookActionObjectProductAddAfter($params)
{
$this->clearMenuCache();
}
public function hookActionCategoryUpdate($params)
{
$this->clearMenuCache();
}
public function hookActionMetaPageSave($params)
{
$this->clearMenuCache();
}
public function hookActionShopDataDuplication($params)
{
$sourceShopId = (int) $params['old_id_shop'];
$targetShopId = (int) $params['new_id_shop'];
$repository = new MenuRepository(Db::getInstance(), $this);
$tree = $repository->getAdminTree($sourceShopId, Language::getLanguages(false));
if (empty($tree)) {
return;
}
$currentShop = $this->context->shop;
$this->context->shop = new Shop($targetShopId);
try {
$this->saveMenuTree($tree);
$this->generateSpriteAssets($targetShopId);
} catch (Exception $exception) {
}
$this->context->shop = $currentShop;
}
private function getAdminAjaxUrl(): string
{
return $this->context->link->getAdminLink('AdminModules', true, [], [
'configure' => $this->name,
'module_name' => $this->name,
'tab_module' => $this->tab,
]);
}
private function buildModuleImageUrl(string $path): string
{
return rtrim($this->getPathUri(), '/') . '/views/img/' . ltrim($path, '/');
}
}