<?php
namespace ZweiPunktVariantenAusgrauen\Subscriber;
use Exception;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Request;
use ZweiPunktVariantenAusgrauen\ZweiPunktVariantenAusgrauen;
/**
* Class ChooseAvailableVariant
*
* Used to set the link to an available variant.
*
* @package ZweiPunktVariantenAusgrauen\Subscriber
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class ChooseAvailableVariant implements EventSubscriberInterface
{
/**
* @var EntityRepositoryInterface
*/
private $productRepository;
/**
* @var RequestStack
*/
protected $requestStack;
/**
* @var array<string, mixed>
*/
private $config;
/**
* Holds the crontroller and action of the current request
*
* @var string
*/
private string $controllerAction = '';
private SalesChannelEntityLoadedEvent $event;
/**
* ChooseAvailableVariant constructor.
*
* @param EntityRepositoryInterface $productRepository
* @param RequestStack $requestStack
* @param SystemConfigService $systemConfigService
*/
public function __construct(
EntityRepositoryInterface $productRepository,
RequestStack $requestStack,
SystemConfigService $systemConfigService
) {
$this->productRepository = $productRepository;
$this->requestStack = $requestStack;
// Get plugin configuration
$this->config = $systemConfigService
->get(ZweiPunktVariantenAusgrauen::PLUGIN_NAME . '.config');
}
/**
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
'sales_channel.product.loaded' => 'getUrlFromAvailableVariant'
];
}
/**
* Determines the id for the url generation of an available variant for a variant product
*
* @param SalesChannelEntityLoadedEvent $event
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function getUrlFromAvailableVariant(
SalesChannelEntityLoadedEvent $event
): void {
$this->event = $event;
try {
$this->checkRequest();
$products = $this->getProducts();
} catch (Exception $exception) {
return;
}
// Goes through all products to determine the parent ids
$productsForNewUrl = [];
foreach ($products as $product) {
try {
$this->checkProduct($product);
} catch (Exception $exception) {
continue;
}
// If a parent id is set on the product, then it must be entered into the array.
if (is_string($product->get('parentId'))) {
$productsForNewUrl[] = $product->get('parentId');
}
// If no parent id is set on the product,
// then it must be checked if the product has a child count and
// accordingly its own id must be entered into the array.
// Both cases can never be true at the same time,
// because variants do not have a child count and main items do not have a parent id.
if ($product->get('childCount') > 0) {
$productsForNewUrl[] = $product->get('id');
}
}
// For the set parent ids, the variants are determined
$siblings = $this->getSiblings(
$productsForNewUrl,
$event->getContext()
)->getEntities();
// For each product, the variants are gone through to determine the ids.
foreach ($products as $product) {
foreach ($siblings as $sibling) {
// Here again, a difference must be made between the main product
// and the variant, since a main item can also be selected in a product box.
// If the parent id of the product matches the parentid of the variant,
// then it is a variant and the id of the variant is added to the product under newUrlId.
if ($product->get('parentId') == $sibling->getParentId()) {
$product->assign(['newUrlId' => $sibling->getId()]);
}
// If the id of the product matches the parentid of the variant,
// then it is a main item and the id of the variant is added to the product under newUrlId.
if ($product->get('id') == $sibling->getParentId()) {
$product->assign(['instockProductId' => $sibling->getId()]);
}
}
}
}
/**
* @param string[] $parentIds
* @param Context $context
* @return EntitySearchResult
*/
private function getSiblings(
array $parentIds,
Context $context
): EntitySearchResult {
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter('parentId', $parentIds));
$criteria->addFilter(
new RangeFilter('availableStock', [
RangeFilter::GT => 0
])
);
return $this
->productRepository
->search($criteria, $context);
}
private function checkRequest(): void
{
// Determines the current controller and its action
$request = $this->requestStack->getCurrentRequest();
// no http protocol / browser request via console calls
if (!$request instanceof Request) {
throw new Exception('No request available');
}
$controllerPath = explode('\\', $request->attributes->get('_controller'));
$this->controllerAction = end($controllerPath);
// Sets the array for the controllers for which a change is to be made
// Allowed controllers and the action are:
// Home page, Listing, CMS Pages with product sliders or boxes, Detail page for cross selling.
$allowedController = [
'NavigationController::home',
'ProductController::index',
'NavigationController::index',
'CmsController::category'
];
// If the current controller is not in the array, the function can be exited
if (false == in_array($this->controllerAction, $allowedController)) {
throw new Exception('Not a valid request');
}
}
/**
* @return ProductEntity[]
* @throws Exception
*/
private function getProducts(): array
{
// The products are determined
$products = $this->event->getEntities();
// If no product is present, the function can be exited
if (empty($products)) {
throw new Exception('No products');
}
// If it is the controller and the action that is responsible for the detail page,
// then the pass can be skipped with only one product,
// since this is the called item.
// Another pass would then be the cross selling,
// which contains more articles.
if (
1 == count($products) &&
'ProductController::index' == $this->controllerAction
) {
throw new Exception('Detail page');
}
return $products;
}
private function checkProduct(ProductEntity $product): void
{
// If the product is not a closeout product,
// but in the configuration only closeout products are marked,
// then it can continue with the next product.
if (
!$product->getIsCloseout() &&
$this->config["onlyCloseoutProducts"]
) {
throw new Exception('Only closeout products');
}
// If the product has an available stock greater than zero
// and a child count of 0, then it is a variant that already has a stock
// and therefore does not need to be considered further.
// If a main item has been selected for a product box and has an available inventory greater than zero,
// then its variants with an inventory must still be checked
// so that a corresponding variant can be linked to.
// Therefore, only the variants are tested for available stock.
if ($product->get('availableStock') > 0 && 0 == $product->get('childCount')) {
throw new Exception('has stock');
}
}
}