<?php
namespace ZweiPunktVariantsTableOverview\Subscriber;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Property\PropertyGroupCollection;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\Unit\UnitEntity;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Framework\Feature;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
/**
* Class AllVariantDetailPage
*
* Used to collect the information of the variants
*/
class AllVariantDetailPage implements EventSubscriberInterface
{
private SalesChannelRepository $productRepository;
private ProductEntity $currentSibling;
public ContainerInterface $container;
/**
* AllVariantDetailPage constructor.
*
* @param SalesChannelRepository $productRepository
* @param ContainerInterface $container
*/
public function __construct(
SalesChannelRepository $productRepository,
ContainerInterface $container
) {
$this->productRepository = $productRepository;
$this->container = $container;
}
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
ProductPageLoadedEvent::class => 'addAllVariantToDetailPage'
];
}
/**
* Used to prepare the information of the variants for the overview table.
*
* @param ProductPageLoadedEvent $event
*/
public function addAllVariantToDetailPage(
ProductPageLoadedEvent $event
): void {
// Determines the current product
$product = $event->getPage()->getProduct();
// Check for a parent product and skip if it's not a variant
$parentId = $product->getParentId();
if (empty($parentId)) {
return;
}
// Get all siblings (products with the same parent)
$siblings = $this->getSiblings(
$parentId,
$event->getSalesChannelContext()
);
$variants = $this->prepareVariantsData(
$siblings,
$event->getPage()->getConfiguratorSettings()
);
// Check if we're running on Shopware 6.5 or newer
if (!Feature::isActive('v6.5.0.0')) {
// Get the CSRF token manager from the container
$csrfTokenManager = $this
->container
->get('security.csrf.token_manager');
// Generate the CSRF token
$tableViewCsrfToken = $csrfTokenManager
->getToken('frontend.checkout.line-item.add')
->getValue();
} else {
$tableViewCsrfToken = null;
}
// The array is passed to the page and
// can be found at page.extensions.allVariants.
$event
->getPage()
->assign([
'allVariants' => $variants,
'variantsTable_csrf_token' => $tableViewCsrfToken
]);
}
/**
* Determines the variants and
* all associated information based on the passed parentId.
*
* @param string $parent
* @param SalesChannelContext $context
* @return EntitySearchResult
*/
private function getSiblings(
string $parent,
SalesChannelContext $context
): EntitySearchResult {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('parentId', $parent))
->addAssociation('manufacturer.media')
->addAssociation('customFields')
->addAssociation('options.group')
->addAssociation('properties.group')
->addAssociation('mainCategories.category')
->addAssociation('deliveryTime.deliveryTime')
->getAssociation('media')
->addAssociation('unit');
return $this->productRepository->search($criteria, $context);
}
/**
* Prepare the variant data for the overview table.
*
* @param EntitySearchResult $siblings
* @param PropertyGroupCollection $configSettings
* @return array<string, mixed>
*/
private function prepareVariantsData(
EntitySearchResult $siblings,
PropertyGroupCollection $configSettings
): array {
$variants = [];
if ($siblings !== null) {
foreach ($siblings->getElements() as $sibling) {
if (!$sibling->getActive()) {
continue;
}
$this->currentSibling = $sibling;
$stock = $sibling->getAvailableStock() >= 1;
$prices = $this->calculateReferencePrice($sibling);
$variantOptions = array_column(
$sibling
->getVariation(),
'option'
);
$image = $this->getProductImage();
// The delivery time is determined.
$deliveryTime = $sibling
->getDeliveryTime() ? $sibling
->getDeliveryTime()
->getName() : null;
// Determines the position of the variant in the order
// in which the variants should later appear
$positions = $this->getPosition($configSettings);
$position = '';
foreach ($sibling->getOptions() as $option) {
foreach ($positions as $index => $positionId) {
if ($option->getId() == $index) {
$position .= $positionId;
}
}
}
$unit = $sibling->getUnit();
$unitDetails = $this->getUnitDetails($unit);
$unitName = $unitDetails['unitName'];
$unitShortCode = $unitDetails['unitShortCode'];
$unitId = $unitDetails['unitId'];
// All information is written to the array
$variants[$position] = [
'media' => $image['image'],
'altTag' => $image['alt'],
'productNumber' => $sibling->getProductNumber(),
'manufacturerNumber' => $sibling->getManufacturerNumber(),
'variation' => null,
'price' => $prices['price'],
'referenceUnitPrice' => $prices['referenceUnitPrice'],
'referenceUnitName' => $prices['referenceUnitName'],
'referenceUnit' => $prices['referenceUnit'],
'inStock' => $stock,
'options' => $variantOptions,
'deliveryTime' => $deliveryTime,
'id' => $sibling->getId(),
'available' => $sibling->getAvailable(),
'minPurchase' => $sibling->getMinPurchase(),
'purchaseUnit' => $sibling->getPurchaseUnit(),
'purchaseUnitName' => $unitName,
'purchaseSteps' => $sibling->getPurchaseSteps(),
'availableStock' => $sibling->getAvailableStock(),
'translated' => $sibling->getTranslated(),
'restockTime' => $sibling->getRestockTime(),
'deliveryTimeTranslation' => $deliveryTime,
'product' => $sibling,
'isCloseout' => $sibling->getIsCloseout(),
'shippingFree' => $sibling->getShippingFree(),
'active' => $sibling->getActive(),
'releaseDate' => $sibling->getReleaseDate(),
'advancedPrices' => $this->prepareAdvancedPrices($siblings)[$sibling->getId()],
'unitId' => $unitId,
'unitShortCode' => $unitShortCode,
'unitName' => $unitName
];
}
}
// Sorts the variants by position to match
// the order of the Configurator options
ksort($variants);
return $variants;
}
/**
* @param UnitEntity $unit
* @return array{unitId: string, unitShortCode: string, unitName: string, }
*/
private function getUnitDetails($unit): array
{
$unitId = '';
$unitShortCode = '';
$unitName = '';
if ($unit instanceof UnitEntity) {
$unitId = $unit->getId();
$unitShortCode = $unit->getTranslated()['shortCode'];
$unitName = $unit->getTranslated()['name'];
}
return ['unitId' => $unitId, 'unitShortCode' => $unitShortCode, 'unitName' => $unitName, ];
}
/**
* Get the product price and reference price.
*
* Warnung wurde nach Rücksprache von dengie und kevbei unterdrückt.
*
* @return array<string, mixed>
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function calculateReferencePrice(
SalesChannelProductEntity $sibling
): array {
$referencePrice = 0;
$referenceUnitName = '';
$referenceUnit = 1;
$calcPrice = $sibling->getCalculatedPrice();
$minQty = $sibling->getMinPurchase();
$calcPrices = $sibling->getCalculatedPrices();
if ($minQty > 1) {
$selectedOption = null;
$nextHigherQty = null;
foreach ($calcPrices as $priceOption) {
$quantity = $priceOption->getQuantity();
if (
$quantity >= $minQty && (
$nextHigherQty === null ||
$quantity < $nextHigherQty
)
) {
$nextHigherQty = $quantity;
$selectedOption = $priceOption;
}
}
if ($selectedOption === null) {
// Kein höherer Preis gefunden, wähle den höchsten kleineren Preis
$maxLowerQty = 0;
foreach ($calcPrices as $priceOption) {
if (
$priceOption
->getQuantity() > $maxLowerQty &&
$priceOption->getQuantity() < $minQty
) {
$maxLowerQty = $priceOption->getQuantity();
$selectedOption = $priceOption;
}
}
}
$price = $selectedOption ? $selectedOption
->getUnitPrice() * $minQty : $calcPrice
->getTotalPrice() * $minQty;
} else {
// Überprüfen, ob getTotalPrice() einen gültigen Wert hat.
$price = (empty($sibling
->getCalculatedPrices()
->first())) ? $sibling
->getCalculatedPrice()
->getTotalPrice() : $sibling
->getCalculatedPrices()
->first()
->getUnitPrice();
// Überprüfen, ob getReferencePrice() einen gültigen Wert hat.
if ($calcPrice->getReferencePrice() !== null) {
$referencePrice = $calcPrice->getReferencePrice()->getPrice();
$referenceUnitName = $calcPrice
->getReferencePrice()
->getUnitName();
}
}
return [
'price' => $price,
'referenceUnitPrice' => $referencePrice,
'referenceUnitName' => $referenceUnitName,
'referenceUnit' => $referenceUnit
];
}
/**
* Get the product image and alt tag.
*
* @return array<string, string>
*/
private function getProductImage(): array
{
$image = null;
$altTag = null;
foreach ($this->currentSibling->getMedia()->getElements() as $media) {
if ($this->currentSibling->getCoverId() == $media->getId()) {
$image = $media->getMedia()->getUrl();
}
if (!empty($media->getMedia()->getAlt())) {
$altTag = $media->getMedia()->getAlt();
} else {
$altTag = $this->currentSibling->getProductNumber();
}
}
return [
'image' => $image,
'alt' => $altTag
];
}
/**
* Sets the order of the Configurator options.
*
* @param PropertyGroupCollection $configSettings
* @return array<string ,int>
*/
private function getPosition(PropertyGroupCollection $configSettings): array
{
$positions = [];
$i = 1;
foreach ($configSettings->getElements() as $configSetting) {
foreach ($configSetting->getOptions()->getElements() as $option) {
$positions[$option->getId()] = $i;
$i++;
}
}
return $positions;
}
/**
* returns the staggered prices including due toeter columns
*
* @return array<string, mixed>
*/
private function prepareAdvancedPrices(
EntitySearchResult $siblings
): array {
$prices = [];
// find the largest number of scales
$pricesCount = 0;
foreach ($siblings as $sibling) {
if (count($sibling->getCalculatedPrices()->getElements()) > $pricesCount) {
$pricesCount = count($sibling->getCalculatedPrices()->getElements());
}
}
// top up for the column edition
foreach ($siblings as $variantId => $sibling) {
$prices[$variantId] = $sibling->getCalculatedPrices()->getElements();
if (count($prices[$variantId]) < $pricesCount) {
for ($i = ($pricesCount - count($prices[$variantId])); $i > 0; $i--) {
$prices[$variantId][] = false;
}
}
}
return $prices;
}
}