336 lines
11 KiB
PHP
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]
|
|
);
|
|
}
|
|
}
|
|
|