*/ 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> $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 $node * @param array $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 $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 $node * * @return array */ 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 $node * * @return array> */ 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 $productIds * * @return array> */ 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> */ 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> $categories * * @return array> */ 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 $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': if ($customLink !== '') { return $customLink; } 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 $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 $node * * @return array */ 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, '/'); } }