Files
2026-09-15 10:35:56 +08:00

496 lines
17 KiB
PHP
Executable File

<?php
/**
* MegaMenu Data ViewModel for Magento 2.4.9
*/
namespace Esun\MegaMenu\ViewModel;
use Esun\Catalog\Model\Product\CardPriceResolver;
use Esun\MegaMenu\Model\Config\Source\MegaMenuType;
use Magento\Catalog\Helper\Image as ImageHelper;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Cms\Model\Template\FilterProvider;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
class MegaMenuData implements ArgumentInterface
{
private const XML_PATH_ENABLED = 'esun_megamenu/general/enabled';
private const XML_PATH_TITLE = 'esun_megamenu/general/title';
private const XML_PATH_ROOT_CATEGORY_ID = 'esun_megamenu/general/root_category_id';
private const XML_PATH_ROOT_CATEGORY_IDS = 'esun_megamenu/general/root_category_ids';
private const XML_PATH_PRODUCTS_PER_SERIES = 'esun_megamenu/general/products_per_series';
private const XML_PATH_SERIES_LIMIT = 'esun_megamenu/general/series_limit';
private $categoryCollectionFactory;
private $productCollectionFactory;
private $imageHelper;
private $jsonSerializer;
private $storeManager;
private $scopeConfig;
private $priceCurrency;
private CardPriceResolver $cardPriceResolver;
private $productRepository;
private $filterProvider;
private $categoryUrlCache = [];
public function __construct(
CategoryCollectionFactory $categoryCollectionFactory,
ProductCollectionFactory $productCollectionFactory,
ImageHelper $imageHelper,
JsonSerializer $jsonSerializer,
StoreManagerInterface $storeManager,
ScopeConfigInterface $scopeConfig,
PriceCurrencyInterface $priceCurrency,
CardPriceResolver $cardPriceResolver,
ProductRepositoryInterface $productRepository,
FilterProvider $filterProvider
) {
$this->categoryCollectionFactory = $categoryCollectionFactory;
$this->productCollectionFactory = $productCollectionFactory;
$this->imageHelper = $imageHelper;
$this->jsonSerializer = $jsonSerializer;
$this->storeManager = $storeManager;
$this->scopeConfig = $scopeConfig;
$this->priceCurrency = $priceCurrency;
$this->cardPriceResolver = $cardPriceResolver;
$this->productRepository = $productRepository;
$this->filterProvider = $filterProvider;
}
public function isEnabled(): bool
{
return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED, ScopeInterface::SCOPE_STORE);
}
public function getTitle(): string
{
return trim((string)$this->scopeConfig->getValue(self::XML_PATH_TITLE, ScopeInterface::SCOPE_STORE)) ?: 'CLASSIFICATION';
}
public function getRootCategoryId(): int
{
return (int)$this->scopeConfig->getValue(self::XML_PATH_ROOT_CATEGORY_ID, ScopeInterface::SCOPE_STORE);
}
public function getRootCategoryIds(): array
{
$configuredIds = (string)$this->scopeConfig->getValue(self::XML_PATH_ROOT_CATEGORY_IDS, ScopeInterface::SCOPE_STORE);
$legacyId = $this->getRootCategoryId();
$ids = [];
foreach (preg_split('/[\s,;]+/', $configuredIds) ?: [] as $value) {
$id = (int)trim($value);
if ($id > 0) {
$ids[$id] = $id;
}
}
if ($legacyId > 0) {
$ids[$legacyId] = $legacyId;
}
return array_values($ids);
}
public function isMegaMenuRootCategory(int $categoryId): bool
{
return in_array($categoryId, $this->getRootCategoryIds(), true);
}
public function getMegaMenuType(int $rootCategoryId): string
{
$category = $this->getCategoryById($rootCategoryId, ['esun_megamenu_type', 'is_active']);
$type = $category ? trim((string)$category->getData('esun_megamenu_type')) : '';
return $type === MegaMenuType::ICON_CATEGORY ? $type : MegaMenuType::PRODUCT;
}
public function isIconCategoryMenu(int $rootCategoryId): bool
{
return $this->getMegaMenuType($rootCategoryId) === MegaMenuType::ICON_CATEGORY;
}
public function getIconCategoryMenuData(int $rootCategoryId): array
{
if (!$rootCategoryId) {
return [];
}
$storeId = (int)$this->storeManager->getStore()->getId();
$columns = $this->categoryCollectionFactory->create()
->setStoreId($storeId)
->addAttributeToSelect([
'name',
'is_active',
'include_in_menu',
'url_key',
'url_path',
'esun_megamenu_icon',
'esun_megamenu_redirect_url'
])
->addAttributeToFilter('parent_id', $rootCategoryId)
->addAttributeToFilter('is_active', 1)
->addAttributeToFilter('include_in_menu', 1)
->setOrder('position', 'ASC');
$menuData = [];
foreach ($columns as $column) {
$links = [];
$children = $this->getChildCategories((int)$column->getId(), $storeId);
foreach ($children as $child) {
$links[] = [
'id' => (int)$child->getId(),
'name' => (string)$child->getName(),
'url' => $this->getConfiguredCategoryUrl($child)
];
}
$menuData[] = [
'id' => (int)$column->getId(),
'name' => (string)$column->getName(),
'url' => $this->getConfiguredCategoryUrl($column),
'icon_url' => $this->getCategoryImageUrl(
trim((string)$column->getData('esun_megamenu_icon'))
),
'children' => $links
];
}
return $menuData;
}
public function getFeaturedProductData(int $rootCategoryId): ?array
{
if (!$rootCategoryId) {
return null;
}
$storeId = (int)$this->storeManager->getStore()->getId();
$category = $this->getCategoryById($rootCategoryId, [
'esun_megamenu_featured_background',
'esun_megamenu_featured_label',
'esun_megamenu_featured_sku',
'is_active'
]);
if (!$category || !$category->getId()) {
return null;
}
$sku = trim((string)$category->getData('esun_megamenu_featured_sku'));
if ($sku === '') {
return null;
}
try {
$product = $this->productRepository->get($sku, false, $storeId);
} catch (NoSuchEntityException $e) {
return null;
}
if ((int)$product->getStatus() !== Status::STATUS_ENABLED) {
return null;
}
$backgroundImage = trim((string)$category->getData('esun_megamenu_featured_background'));
$label = trim((string)$category->getData('esun_megamenu_featured_label')) ?: 'New Arrival';
return [
'label' => $label,
'name' => (string)$product->getName(),
'url' => $product->getProductUrl(),
'category_background_url' => $this->getCategoryImageUrl($backgroundImage)
];
}
public function getTopLevelCategoryUrl(int $categoryId, string $fallbackUrl = '#'): string
{
if (!$categoryId || $this->isMegaMenuRootCategory($categoryId)) {
return $fallbackUrl;
}
if (isset($this->categoryUrlCache[$categoryId])) {
return $this->categoryUrlCache[$categoryId];
}
$category = $this->getCategoryById($categoryId, [
'esun_megamenu_redirect_url',
'url_key',
'url_path',
'is_active'
]);
if (!$category || !$category->getId()) {
return $fallbackUrl;
}
$redirectUrl = trim((string)$category->getData('esun_megamenu_redirect_url'));
$resolvedUrl = $redirectUrl !== ''
? $this->resolveRedirectUrl($redirectUrl)
: $category->getUrl();
$this->categoryUrlCache[$categoryId] = $resolvedUrl ?: $fallbackUrl;
return $this->categoryUrlCache[$categoryId];
}
private function getConfiguredCategoryUrl(Category $category): string
{
$redirectUrl = trim((string)$category->getData('esun_megamenu_redirect_url'));
return $redirectUrl !== '' ? $this->resolveRedirectUrl($redirectUrl) : $category->getUrl();
}
public function getRootCategoryUrl(?int $rootCategoryId = null): string
{
$rootCategoryId = $rootCategoryId ?: $this->getRootCategoryId();
if (!$rootCategoryId) {
return '#';
}
$category = $this->getCategoryById($rootCategoryId, [
'esun_megamenu_redirect_url',
'is_active',
'url_key',
'url_path'
]);
return $category && $category->getId() ? $this->getConfiguredCategoryUrl($category) : '#';
}
public function getRootCategoryName(int $rootCategoryId): string
{
if (!$rootCategoryId) {
return $this->getTitle();
}
$category = $this->getCategoryById($rootCategoryId, ['name', 'is_active']);
return $category && $category->getId() ? (string)$category->getName() : $this->getTitle();
}
public function getRootCategoryDescription(int $rootCategoryId): string
{
if (!$rootCategoryId) {
return '';
}
$category = $this->getCategoryById($rootCategoryId, ['description', 'is_active']);
if (!$category || !$category->getId()) {
return '';
}
$description = trim((string)$category->getDescription());
return $description !== ''
? (string)$this->filterProvider->getPageFilter()->filter($description)
: '';
}
public function getProductsPerSeries(): int
{
$limit = (int)$this->scopeConfig->getValue(self::XML_PATH_PRODUCTS_PER_SERIES, ScopeInterface::SCOPE_STORE);
return $limit > 0 ? $limit : 6;
}
public function getSeriesLimit(): int
{
$limit = (int)$this->scopeConfig->getValue(self::XML_PATH_SERIES_LIMIT, ScopeInterface::SCOPE_STORE);
return $limit > 0 ? $limit : 8;
}
public function getMegaMenuJson(?int $rootCategoryId = null): string
{
return $this->jsonSerializer->serialize($this->getMegaMenuData($rootCategoryId));
}
public function getMegaMenuData(?int $rootCategoryId = null): array
{
$rootCategoryId = $rootCategoryId ?: $this->getRootCategoryId();
if (!$rootCategoryId) {
return [];
}
$storeId = $this->storeManager->getStore()->getId();
$filamentCategories = $this->categoryCollectionFactory->create()
->setStoreId($storeId)
->addAttributeToSelect([
'name',
'is_active',
'include_in_menu',
'url_key',
'url_path',
'esun_megamenu_redirect_url'
])
->addAttributeToFilter('parent_id', $rootCategoryId)
->addAttributeToFilter('is_active', 1)
->setOrder('position', 'ASC');
$menuData = [];
foreach ($filamentCategories as $filamentCategory) {
$filamentData = [
'id' => (int)$filamentCategory->getId(),
'name' => (string)$filamentCategory->getName(),
'url' => $filamentCategory->getUrl(),
'seriesCategories' => []
];
$seriesCategories = $this->getChildCategories((int)$filamentCategory->getId(), $storeId, $this->getSeriesLimit());
foreach ($seriesCategories as $seriesCategory) {
$filamentData['seriesCategories'][] = $this->buildSeriesCategoryData($seriesCategory);
}
// Some menu roots, such as Resins, assign products directly to their
// first-level child categories instead of using a series level.
if (!$filamentData['seriesCategories']) {
$filamentData['seriesCategories'][] = $this->buildSeriesCategoryData($filamentCategory);
}
$menuData[] = $filamentData;
}
return $menuData;
}
private function getChildCategories(int $parentId, int $storeId, int $pageSize = 0)
{
$collection = $this->categoryCollectionFactory->create()
->setStoreId($storeId)
->addAttributeToSelect([
'name',
'is_active',
'include_in_menu',
'url_key',
'url_path',
'esun_megamenu_redirect_url'
])
->addAttributeToFilter('parent_id', $parentId)
->addAttributeToFilter('is_active', 1)
->addAttributeToFilter('include_in_menu', 1)
->setOrder('position', 'ASC');
if ($pageSize > 0) {
$collection->setPageSize($pageSize)->setCurPage(1);
}
return $collection;
}
private function buildSeriesCategoryData(Category $category): array
{
return [
'id' => (int)$category->getId(),
'name' => (string)$category->getName(),
'url' => $category->getUrl(),
'products' => $this->getProductsForCategory($category)
];
}
private function getProductsForCategory(Category $category): array
{
$storeId = $this->storeManager->getStore()->getId();
$collection = $this->productCollectionFactory->create()
->setStoreId($storeId)
->addStoreFilter($storeId)
->addAttributeToSelect(['name', 'price', 'small_image', 'thumbnail'])
->addFinalPrice()
->addCategoryFilter($category)
->addAttributeToFilter('status', Status::STATUS_ENABLED)
->setVisibility([Visibility::VISIBILITY_IN_CATALOG, Visibility::VISIBILITY_BOTH])
->addUrlRewrite()
->setPageSize($this->getProductsPerSeries())
->setCurPage(1);
$products = [];
foreach ($collection as $product) {
$products[] = [
'id' => (int)$product->getId(),
'name' => (string)$product->getName(),
'url' => $product->getProductUrl(),
'price' => $this->priceCurrency->format(
$this->cardPriceResolver->getFinalPrice($product),
false
),
'image_url' => $this->imageHelper->init($product, 'category_page_grid')->getUrl()
];
}
return $products;
}
private function getCategoryById(int $categoryId, array $attributes): ?Category
{
$storeId = $this->storeManager->getStore()->getId();
$collection = $this->categoryCollectionFactory->create()
->setStoreId($storeId)
->addAttributeToSelect($attributes)
->addAttributeToFilter('entity_id', $categoryId)
->addAttributeToFilter('is_active', 1)
->setPageSize(1);
$category = $collection->getFirstItem();
return $category && $category->getId() ? $category : null;
}
private function getCategoryImageUrl(string $image): string
{
if ($image === '') {
return '';
}
if (preg_match('#^https?://#i', $image)) {
return $image;
}
$image = ltrim($image, '/');
if (strpos($image, 'media/') === 0) {
$image = substr($image, strlen('media/'));
}
$mediaPath = strpos($image, 'catalog/category/') === 0
? $image
: 'catalog/category/' . $image;
return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $mediaPath;
}
private function resolveRedirectUrl(string $redirectUrl): string
{
if (preg_match('#^https?://#i', $redirectUrl)) {
return $redirectUrl;
}
$path = ltrim($redirectUrl, '/');
return rtrim(
$this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_LINK),
'/'
) . '/' . $path;
}
}