393 lines
13 KiB
PHP
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()
|
|
);
|
|
}
|
|
}
|