<?php
declare(strict_types=1);
namespace ZweiPunktVariantenAusgrauen\Subscriber;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use ZweiPunktVariantenAusgrauen\ZweiPunktVariantenAusgrauen;
/**
* Class AddStockToDetailPage
*
* Is used to have the stock numbers of the variants submitted to the frontend
* when loading the detail page.
*
* @package ZweiPunktVariantenAusgrauen\Subscriber
*/
class AddStockToDetailPage implements EventSubscriberInterface
{
/**
* @var mixed
*/
private $config;
private Connection $connection;
/**
* AddStockToDetailPage constructor.
*/
public function __construct(
SystemConfigService $systemConfigService,
Connection $connection
) {
// Get plugin configuration
$this->config = $systemConfigService
->get(ZweiPunktVariantenAusgrauen::PLUGIN_NAME . '.config');
$this->connection = $connection;
}
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
ProductPageLoadedEvent::class => 'onProductPageLoaded'
];
}
/**
* This subscriber determens unavailable variants (siblings) and passes
* unavailable option ids to the theme where the standard shopware logic is
* used gray out the unavailable options.
*
* How it is done:
* If we have only 1 option group:
* Check the stock of all other siblings and gray them out
*
* If we have 2 option groups or more (i.e. color, size, material):
* We want to gray out all siblings that are only one change (color, size
* or material) away from the current product. We have to iterate over
* every group and check if there are siblings with another option in the
* current group but every other group/option is matching the current
* product. Then check if this sibling has stock and if not, save the
* option id in an array that we pass to the theme.
*
* @param ProductPageLoadedEvent $event
*/
public function onProductPageLoaded(ProductPageLoadedEvent $event): void
{
$product = $event
->getPage()
->getProduct();
// Check for a parent product and skip if it's not a variant
$parentId = $product->getParentId();
if (empty($parentId)) {
return;
}
// Ermittelt die Options IDs der Produkte die (je nach Konfiguration)
// keinen (verfügbaren) Bestand haben.
$optionIds = $this->getOptionIds($parentId);
if (empty($optionIds)) {
return;
}
$unavailable = $this->getGreyOutOptions(
$product,
$optionIds
);
// Handling selection fields
$hideSelection = 'hide' == $this->config["behaviorSelectionAttributes"];
$event
->getPage()
->assign([
'unavailable' => $unavailable,
'hideselection' => $hideSelection
])
;
}
/**
* Liefert die Option IDs der Produkte die aktuell keinen Bestand haben.
*
* @param string $parent
* @return array<int, array<string, mixed>>
*/
private function getOptionIds(string $parent): array
{
$stockType = ('availableStock' == $this->config['stockType']) ? 'available_stock' : 'stock';
return $this->connection->fetchAll(
'SELECT product.is_closeout,
product.option_ids
FROM product
WHERE product.' . $stockType . ' < 1
AND product.parent_id = UNHEX(:parent)',
['parent' => $parent]
);
}
/**
* @param SalesChannelProductEntity $product
* @param array<int, array<string, mixed>> $siblings
* @return array<int, string>
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @throws \JsonException
*/
private function getGreyOutOptions(
SalesChannelProductEntity $product,
array $siblings
): array {
// Keeps all the options that are unavailable
$unavailable = [];
// Options for the main product
$optionIds = $product
->getOptionIds()
;
// Iterate over all options (not groups)
// start with the first option / group
foreach ($optionIds as $index => $optionId) {
// create a clone of the array that holds all option ids
$allOptions = $optionIds;
// we remove the first option/group because we want to check the
// stock on siblings with a different option in this group,
// but every other option (in other groups) has to be the same
unset($allOptions[$index]);
// iterate over all siblings with another option for the group
/** @var ProductEntity $sibling */
foreach ($siblings as $sibling) {
// Check if only clouseout products (Abverkaufsartikel) shoud be
// marked and skip the product if necessary.
if (
false == $product->getIsCloseout()
&& (null == $sibling['is_closeout'] || 0 == $sibling['is_closeout'])
&& true == $this->config["onlyCloseoutProducts"]
) {
continue;
}
// do this on all siblings which have a different option
// in the current group. This will give us a sibling with only
// ONE option different to the main product.
if (!in_array($optionId, json_decode($sibling['option_ids'], true, 512, JSON_THROW_ON_ERROR))) {
// 1. get all sibling option ids
// 2. remove the matching options
// 3. the one difference will remain
$siblingOptions = json_decode($sibling['option_ids'], true, 512, JSON_THROW_ON_ERROR);
// If there is more then one option
// check if all other options match or
// procceed with the next sibling
if (count($siblingOptions) > 1) {
foreach ($allOptions as $otherOption) {
if (false == in_array($otherOption, $siblingOptions)) {
continue 2;
}
// remove the matched option from the array
if (false !== ($i = array_search($otherOption, $siblingOptions))) {
unset($siblingOptions[$i]);
}
}
}
// the current option
$unavailable[] = array_pop($siblingOptions);
}
}
}
return $unavailable;
}
}