Files
drmaterac.pl/modules/crosssellpro/crosssellpro.php
2026-04-01 12:48:10 +02:00

336 lines
11 KiB
PHP

<?php
/**
* Cross Sell PRO module for cart page upsell.
*
* @author Pyziak Jacek
* @copyright project-pro.pl
* @link https://www.project-pro.pl
*/
use PrestaShop\PrestaShop\Adapter\Image\ImageRetriever;
use PrestaShop\PrestaShop\Adapter\Product\PriceFormatter;
use PrestaShop\PrestaShop\Adapter\Product\ProductColorsRetriever;
if (!defined('_PS_VERSION_')) {
exit;
}
class Crosssellpro extends Module
{
const DEFAULT_LIMIT = 12;
public function __construct()
{
$this->name = 'crosssellpro';
$this->tab = 'pricing_promotion';
$this->version = '1.1.6';
$this->author = 'Pyziak Jacek';
$this->need_instance = 0;
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('Cross Sell PRO');
$this->description = $this->l('Displays related products carousel in cart based on product associations.');
}
public function install()
{
return parent::install()
&& $this->registerHook('displayShoppingCartFooter')
&& $this->registerHook('displayCheckoutSummaryTop')
&& $this->registerHook('displayHeader')
&& $this->registerHook('actionFrontControllerSetMedia');
}
public function uninstall()
{
return parent::uninstall();
}
public function hookActionFrontControllerSetMedia()
{
if (!$this->context || !$this->context->controller) {
return;
}
if (!$this->isRegisteredInHook('displayCheckoutSummaryTop')) {
$this->registerHook('displayCheckoutSummaryTop');
}
if (!$this->isRegisteredInHook('displayHeader')) {
$this->registerHook('displayHeader');
}
$controllerName = (string) $this->context->controller->php_self;
if (!in_array($controllerName, ['cart', 'order'], true)) {
return;
}
$this->registerAssets();
}
public function hookDisplayHeader()
{
if (!$this->context || !$this->context->controller) {
return;
}
$controllerName = (string) $this->context->controller->php_self;
if (!in_array($controllerName, ['cart', 'order'], true)) {
return;
}
$this->registerAssets();
}
public function hookDisplayShoppingCartFooter($params)
{
if (!$this->context || !$this->context->cart || !$this->context->cart->id) {
return '';
}
$products = $this->buildCrossSellProducts();
if (empty($products)) {
return '';
}
$this->context->smarty->assign([
'crosssellpro_products' => $products,
'crosssellpro_cart_url' => $this->context->link->getPageLink('cart', true),
'crosssellpro_return_url' => $this->context->link->getPageLink('cart', true, null, 'action=show'),
'crosssellpro_mode' => 'cart',
]);
return $this->fetch('module:' . $this->name . '/views/templates/hook/cartCrossSell.tpl');
}
public function hookDisplayCheckoutSummaryTop($params)
{
if (!$this->context || !$this->context->cart || !$this->context->cart->id) {
return '';
}
$products = $this->buildCrossSellProducts();
if (empty($products)) {
return '';
}
$this->context->smarty->assign([
'crosssellpro_products' => $products,
'crosssellpro_cart_url' => $this->context->link->getPageLink('cart', true),
'crosssellpro_return_url' => $this->context->link->getPageLink('order', true),
'crosssellpro_mode' => 'checkout',
]);
return $this->fetch('module:' . $this->name . '/views/templates/hook/checkoutCrossSell.tpl');
}
/**
* Builds presented products list from accessories of products currently in cart.
*
* @return array
*/
protected function buildCrossSellProducts()
{
$cartProducts = $this->context->cart->getProducts(true);
if (empty($cartProducts)) {
return [];
}
$inCartProductIds = [];
foreach ($cartProducts as $cartProduct) {
$inCartProductIds[(int) $cartProduct['id_product']] = true;
}
$relatedIds = $this->collectAccessoryIds(array_keys($inCartProductIds));
if (empty($relatedIds)) {
return [];
}
$relatedIds = array_values(array_diff($relatedIds, array_keys($inCartProductIds)));
if (empty($relatedIds)) {
return [];
}
$limitedIds = array_slice($relatedIds, 0, static::DEFAULT_LIMIT);
$products = $this->presentProducts($limitedIds);
if (empty($products)) {
return [];
}
$combinationFlags = $this->getCombinationFlags($limitedIds);
foreach ($products as &$product) {
$productId = (int) $product['id_product'];
$requiresSelection = !empty($combinationFlags[$productId]) || empty($product['add_to_cart_url']);
$product['crosssellpro_requires_selection'] = $requiresSelection;
if ($requiresSelection) {
$product['crosssellpro_cta_url'] = $product['url'];
$product['crosssellpro_cta_label'] = $this->l('Wybierz wariant');
} else {
$qty = 1;
if (isset($product['minimal_quantity']) && (int) $product['minimal_quantity'] > 0) {
$qty = (int) $product['minimal_quantity'];
}
$product['crosssellpro_post_add_url'] = $this->context->link->getPageLink(
'cart',
true,
null,
'add=1&id_product=' . $productId . '&qty=' . $qty . '&token=' . Tools::getToken(false)
);
$product['crosssellpro_cta_url'] = $this->context->link->getPageLink('cart', true, null, 'action=show');
$product['crosssellpro_cta_label'] = $this->l('Dodaj do koszyka');
}
}
unset($product);
return $products;
}
/**
* @param int[] $productIds
*
* @return int[]
*/
protected function collectAccessoryIds(array $productIds)
{
$ids = [];
foreach ($productIds as $productId) {
$accessories = Product::getAccessoriesLight(
(int) $this->context->language->id,
(int) $productId,
(Context::getContext() === null ? $this->context : Context::getContext())
);
if (empty($accessories)) {
continue;
}
foreach ($accessories as $accessory) {
if (!isset($accessory['id_product'])) {
continue;
}
$accessoryId = (int) $accessory['id_product'];
if ($accessoryId > 0) {
$ids[$accessoryId] = $accessoryId;
}
}
}
return array_values($ids);
}
/**
* @param int[] $productIds
*
* @return array<int, bool>
*/
protected function getCombinationFlags(array $productIds)
{
if (empty($productIds)) {
return [];
}
$rows = Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS(
'SELECT p.id_product, COUNT(pa.id_product_attribute) AS combinations
FROM `' . _DB_PREFIX_ . 'product` p
LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (pa.id_product = p.id_product)
WHERE p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')
GROUP BY p.id_product'
);
$flags = [];
foreach ($rows as $row) {
$flags[(int) $row['id_product']] = ((int) $row['combinations'] > 0);
}
return $flags;
}
/**
* @param int[] $productIds
*
* @return array
*/
protected function presentProducts(array $productIds)
{
if (empty($productIds)) {
return [];
}
$rawProducts = Product::getProductsProperties(
(int) $this->context->language->id,
Db::getInstance((bool) _PS_USE_SQL_SLAVE_)->executeS(
'SELECT p.id_product
FROM `' . _DB_PREFIX_ . 'product` p
' . Shop::addSqlAssociation('product', 'p') . '
WHERE p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')
AND p.active = 1
AND product_shop.visibility IN ("both", "catalog")
ORDER BY FIELD(p.id_product,' . implode(',', array_map('intval', $productIds)) . ')'
)
);
if (empty($rawProducts)) {
return [];
}
$assembler = new ProductAssembler($this->context);
$presenterFactory = new ProductPresenterFactory($this->context);
$presentationSettings = $presenterFactory->getPresentationSettings();
$presentationSettings->showPrices = true;
if (version_compare(_PS_VERSION_, '1.7.5', '>=')) {
$presenter = new \PrestaShop\PrestaShop\Adapter\Presenter\Product\ProductListingPresenter(
new ImageRetriever($this->context->link),
$this->context->link,
new PriceFormatter(),
new ProductColorsRetriever(),
$this->context->getTranslator()
);
} else {
$presenter = new \PrestaShop\PrestaShop\Core\Product\ProductListingPresenter(
new ImageRetriever($this->context->link),
$this->context->link,
new PriceFormatter(),
new ProductColorsRetriever(),
$this->context->getTranslator()
);
}
$productsForTemplate = [];
foreach ($rawProducts as $rawProduct) {
$presented = $presenter->present(
$presentationSettings,
$assembler->assembleProduct($rawProduct),
$this->context->language
);
if (!empty($presented['add_to_cart_url']) || !empty($presented['url'])) {
$productsForTemplate[] = $presented;
}
}
return $productsForTemplate;
}
protected function registerAssets()
{
$this->context->controller->registerStylesheet(
'module-crosssellpro-cart',
'modules/' . $this->name . '/views/css/cartCrossSell.css',
['media' => 'all', 'priority' => 150]
);
$this->context->controller->registerJavascript(
'module-crosssellpro-cart',
'modules/' . $this->name . '/views/js/cartCrossSell.js',
['position' => 'bottom', 'priority' => 150]
);
}
}