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

393 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Homepage\Block;
use Esun\Catalog\Model\Product\CardPriceResolver;
use Magento\Catalog\Block\Product\AbstractProduct;
use Magento\Catalog\Block\Product\Context;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\CatalogRule\Model\Rule;
use Magento\CatalogRule\Model\RuleFactory;
use Magento\Customer\Model\Context as CustomerContext;
use Magento\Framework\App\Http\Context as HttpContext;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Pricing\Render;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Swatches\Helper\Data as SwatchHelper;
use Magento\Swatches\Helper\Media as SwatchMediaHelper;
use Magento\Swatches\Model\Swatch;
class CatalogRuleDeal extends AbstractProduct implements IdentityInterface
{
private const XML_PATH = 'esun_homepage/catalog_rule_deal/';
private const MAX_PRODUCTS = 12;
private ?Rule $rule = null;
private ?Collection $products = null;
public function __construct(
Context $context,
private readonly RuleFactory $ruleFactory,
private readonly CollectionFactory $productCollectionFactory,
private readonly ResourceConnection $resourceConnection,
private readonly Visibility $productVisibility,
private readonly HttpContext $httpContext,
private readonly TimezoneInterface $timezone,
private readonly SwatchHelper $swatchHelper,
private readonly SwatchMediaHelper $swatchMediaHelper,
private readonly PriceCurrencyInterface $priceCurrency,
private readonly CardPriceResolver $cardPriceResolver,
array $data = []
) {
parent::__construct($context, $data);
}
public function isAvailable(): bool
{
return $this->isEnabled()
&& $this->getRule() !== null
&& $this->isRuleCurrent($this->getRule())
&& $this->getProducts()->getSize() > 0;
}
public function getRule(): ?Rule
{
if ($this->rule !== null) {
return $this->rule->getId() ? $this->rule : null;
}
$ruleId = (int) $this->getConfig('rule_id');
$this->rule = $this->ruleFactory->create();
if ($ruleId > 0) {
$this->rule->load($ruleId);
}
return $this->rule->getId() ? $this->rule : null;
}
public function getProducts(): Collection
{
if ($this->products !== null) {
return $this->products;
}
$collection = $this->productCollectionFactory->create();
$collection->setStoreId((int) $this->_storeManager->getStore()->getId());
$productIds = $this->getIndexedProductIds();
if (!$productIds) {
$collection->addFieldToFilter('entity_id', ['in' => [0]]);
$this->products = $collection;
return $this->products;
}
$this->_addProductAttributesAndPrices($collection);
$collection->addStoreFilter();
$collection->addFieldToFilter('entity_id', ['in' => $productIds]);
$collection->addAttributeToFilter(
'visibility',
['in' => $this->productVisibility->getVisibleInSiteIds()]
);
$collection->addAttributeToFilter('status', Product\Attribute\Source\Status::STATUS_ENABLED);
$collection->setOrder('entity_id', 'DESC');
$collection->setPageSize($this->getProductsLimit());
$collection->setCurPage(1);
$this->products = $collection;
return $this->products;
}
public function getHeading(): string
{
$heading = trim((string) $this->getConfig('title'));
$rule = $this->getRule();
return $heading !== '' ? $heading : (string) ($rule?->getName() ?: '');
}
public function getAccentHeading(): string
{
return trim((string) $this->getConfig('accent_title'));
}
public function getCountdownEnd(): string
{
$toDate = (string) ($this->getRule()?->getToDate() ?: '');
if ($toDate === '') {
return '';
}
$date = $this->timezone->date($toDate . ' 23:59:59');
return $date->format(\DateTimeInterface::ATOM);
}
public function getProductDiscount(Product $product): int
{
$regularPrice = $this->getRegularPriceValue($product);
$finalPrice = $this->getFinalPriceValue($product);
if ($regularPrice <= 0 || $finalPrice >= $regularPrice) {
return 0;
}
return (int) round((1 - ($finalPrice / $regularPrice)) * 100);
}
public function getMaximumDiscount(): int
{
$maximum = 0;
foreach ($this->getProducts() as $product) {
$maximum = max($maximum, $this->getProductDiscount($product));
}
return $maximum;
}
public function getColorSwatches(Product $product): array
{
if ($product->getTypeId() !== 'configurable') {
return [];
}
$children = [];
$optionIds = [];
$childIdsByGroup = $product->getTypeInstance()->getChildrenIds((int) $product->getId());
$childIds = array_values(array_unique(array_map(
'intval',
array_merge(...array_values($childIdsByGroup ?: [[]]))
)));
if (!$childIds) {
return [];
}
$childCollection = $this->productCollectionFactory->create();
$childCollection->setStoreId((int) $this->_storeManager->getStore()->getId());
$childCollection->addIdFilter($childIds);
$childCollection->addAttributeToSelect(['color', 'sku', 'image', 'small_image', 'thumbnail']);
$childCollection->addAttributeToFilter(
'status',
Product\Attribute\Source\Status::STATUS_ENABLED
);
$childCollection->setOrder('entity_id', 'ASC');
foreach ($childCollection as $childProduct) {
$optionId = (int) $childProduct->getData('color');
if ($optionId <= 0) {
continue;
}
$children[] = [
'option_id' => $optionId,
'sku' => (string) $childProduct->getSku(),
'product' => $childProduct,
];
$optionIds[$optionId] = $optionId;
}
if (!$children) {
return [];
}
$swatchData = $this->swatchHelper->getSwatchesByOptionsId(array_values($optionIds));
$colorAttribute = null;
foreach ($product->getTypeInstance()->getConfigurableAttributes($product) as $attribute) {
if ($attribute->getProductAttribute()->getAttributeCode() === 'color') {
$colorAttribute = $attribute->getProductAttribute();
$colorAttribute->setStoreId((int) $this->_storeManager->getStore()->getId());
break;
}
}
$result = [];
foreach ($children as $child) {
$optionId = $child['option_id'];
$swatch = $swatchData[$optionId] ?? [];
$type = (int) ($swatch['type'] ?? Swatch::SWATCH_TYPE_EMPTY);
$value = (string) ($swatch['value'] ?? '');
$result[] = [
'id' => $optionId,
'sku' => $child['sku'],
'label' => $colorAttribute
? (string) $colorAttribute->getSource()->getOptionText($optionId)
: $child['sku'],
'type' => $type,
'value' => $value,
'product_image' => $this->getProductImageUrl($child['product']),
'image' => $type === Swatch::SWATCH_TYPE_VISUAL_IMAGE && $value !== ''
? $this->swatchMediaHelper->getSwatchAttributeImage(
Swatch::SWATCH_IMAGE_NAME,
$value
)
: '',
];
}
return $result;
}
public function getProductImageUrl(Product $product, string $imageId = 'category_page_grid'): string
{
return (string) $this->getImage($product, $imageId)->getImageUrl();
}
public function getFormattedFinalPrice(Product $product): string
{
return $this->formatPrice($this->getFinalPriceValue($product));
}
public function getFormattedRegularPrice(Product $product): string
{
return $this->formatPrice($this->getRegularPriceValue($product));
}
public function hasRegularPrice(Product $product): bool
{
return $this->getRegularPriceValue($product) > $this->getFinalPriceValue($product);
}
public function getProductPrice(Product $product): string
{
return $this->getProductPriceHtml(
$product,
FinalPrice::PRICE_CODE,
Render::ZONE_ITEM_LIST,
[
'include_container' => true,
'display_minimal_price' => true,
'list_category_page' => true,
]
);
}
public function getCacheKeyInfo(): array
{
return [
'ESUN_HOMEPAGE_CATALOG_RULE_DEAL',
(int) $this->_storeManager->getStore()->getId(),
(int) $this->httpContext->getValue(CustomerContext::CONTEXT_GROUP),
(int) ($this->getRule()?->getId() ?: 0),
];
}
public function getIdentities(): array
{
$identities = [];
$rule = $this->getRule();
if ($rule) {
$identities = array_merge($identities, $rule->getIdentities());
}
foreach ($this->getProducts() as $product) {
$identities = array_merge($identities, $product->getIdentities());
}
return array_values(array_unique($identities));
}
private function isEnabled(): bool
{
return $this->_scopeConfig->isSetFlag(
self::XML_PATH . 'enabled',
ScopeInterface::SCOPE_STORE
);
}
private function getConfig(string $field): mixed
{
return $this->_scopeConfig->getValue(
self::XML_PATH . $field,
ScopeInterface::SCOPE_STORE
);
}
private function getProductsLimit(): int
{
$limit = (int) $this->getConfig('products_limit');
return max(1, min(self::MAX_PRODUCTS, $limit ?: 4));
}
private function isRuleCurrent(Rule $rule): bool
{
if (!$rule->getIsActive()) {
return false;
}
$websiteId = (int) $this->_storeManager->getStore()->getWebsiteId();
if (!in_array($websiteId, array_map('intval', (array) $rule->getWebsiteIds()), true)) {
return false;
}
$today = $this->timezone->date()->format('Y-m-d');
$fromDate = (string) $rule->getFromDate();
$toDate = (string) $rule->getToDate();
return ($fromDate === '' || $fromDate <= $today)
&& ($toDate === '' || $toDate >= $today);
}
private function getIndexedProductIds(): array
{
$rule = $this->getRule();
if (!$rule) {
return [];
}
$connection = $this->resourceConnection->getConnection();
$table = $this->resourceConnection->getTableName('catalogrule_product');
$now = time();
$select = $connection->select()
->distinct()
->from($table, ['product_id'])
->where('rule_id = ?', (int) $rule->getId())
->where('website_id = ?', (int) $this->_storeManager->getStore()->getWebsiteId())
->where(
'customer_group_id = ?',
(int) $this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)
)
->where('(from_time = 0 OR from_time <= ?)', $now)
->where('(to_time = 0 OR to_time >= ?)', $now);
$productIds = array_map('intval', $connection->fetchCol($select));
if (!$productIds) {
return [];
}
$parentSelect = $connection->select()
->distinct()
->from(
$this->resourceConnection->getTableName('catalog_product_super_link'),
['parent_id']
)
->where('product_id IN (?)', $productIds);
$parentIds = array_map('intval', $connection->fetchCol($parentSelect));
return array_values(array_unique(array_merge($productIds, $parentIds)));
}
private function getFinalPriceValue(Product $product): float
{
return $this->cardPriceResolver->getFinalPrice($product);
}
private function getRegularPriceValue(Product $product): float
{
return $this->cardPriceResolver->getRegularPrice($product);
}
private function formatPrice(float $price): string
{
return $this->priceCurrency->format(
$price,
false,
PriceCurrencyInterface::DEFAULT_PRECISION,
$this->_storeManager->getStore()
);
}
}