496 lines
17 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|