<?php declare(strict_types=1);
namespace Shopware\Storefront\Page\Product;
use Shopware\Core\Content\Category\Exception\CategoryNotFoundException;
use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaCollection;
use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaEntity;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Content\Product\SalesChannel\CrossSelling\AbstractProductCrossSellingRoute;
use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractProductDetailRoute;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
use Shopware\Core\Content\Property\PropertyGroupCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\Routing\Exception\MissingRequestParameterException;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\GenericPageLoaderInterface;
use Shopware\Storefront\Page\Product\Review\ProductReviewLoader;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
class ProductPageLoader
{
private GenericPageLoaderInterface $genericLoader;
private EventDispatcherInterface $eventDispatcher;
private AbstractProductDetailRoute $productDetailRoute;
private ProductReviewLoader $productReviewLoader;
private AbstractProductCrossSellingRoute $crossSellingRoute;
/**
* @internal
*/
public function __construct(
GenericPageLoaderInterface $genericLoader,
EventDispatcherInterface $eventDispatcher,
AbstractProductDetailRoute $productDetailRoute,
ProductReviewLoader $productReviewLoader,
AbstractProductCrossSellingRoute $crossSellingRoute
) {
$this->genericLoader = $genericLoader;
$this->eventDispatcher = $eventDispatcher;
$this->productDetailRoute = $productDetailRoute;
$this->productReviewLoader = $productReviewLoader;
$this->crossSellingRoute = $crossSellingRoute;
}
/**
* @throws CategoryNotFoundException
* @throws InconsistentCriteriaIdsException
* @throws MissingRequestParameterException
* @throws ProductNotFoundException
*/
public function load(Request $request, SalesChannelContext $context): ProductPage
{
$productId = $request->attributes->get('productId');
if (!$productId) {
throw new MissingRequestParameterException('productId', '/productId');
}
$criteria = (new Criteria())
->addAssociation('manufacturer.media')
->addAssociation('options.group')
->addAssociation('properties.group')
->addAssociation('mainCategories.category')
->addAssociation('media');
$this->eventDispatcher->dispatch(new ProductPageCriteriaEvent($productId, $criteria, $context));
$result = $this->productDetailRoute->load($productId, $request, $context, $criteria);
$product = $result->getProduct();
if ($product->getMedia()) {
$product->getMedia()->sort(function (ProductMediaEntity $a, ProductMediaEntity $b) {
return $a->getPosition() <=> $b->getPosition();
});
}
if ($product->getMedia() && $product->getCover()) {
$product->setMedia(new ProductMediaCollection(array_merge(
[$product->getCover()->getId() => $product->getCover()],
$product->getMedia()->getElements()
)));
}
if ($category = $product->getSeoCategory()) {
$request->request->set('navigationId', $category->getId());
}
$page = $this->genericLoader->load($request, $context);
/** @var ProductPage $page */
$page = ProductPage::createFrom($page);
$page->setProduct($product);
$page->setConfiguratorSettings($result->getConfigurator() ?? new PropertyGroupCollection());
$page->setNavigationId($product->getId());
$this->loadDefaultAdditions($product, $page, $request, $context);
$this->loadOptions($page);
$this->loadMetaData($page);
$this->eventDispatcher->dispatch(
new ProductPageLoadedEvent($page, $context, $request)
);
return $page;
}
private function loadOptions(ProductPage $page): void
{
$options = new PropertyGroupOptionCollection();
$optionIds = $page->getProduct()->getOptionIds() ?? [];
foreach ($page->getConfiguratorSettings() as $group) {
$groupOptions = $group->getOptions();
if ($groupOptions === null) {
continue;
}
foreach ($optionIds as $optionId) {
$groupOption = $groupOptions->get($optionId);
if ($groupOption !== null) {
$options->add($groupOption);
}
}
}
$page->setSelectedOptions($options);
}
private function loadMetaData(ProductPage $page): void
{
$metaInformation = $page->getMetaInformation();
if (!$metaInformation) {
return;
}
$metaDescription = $page->getProduct()->getTranslation('metaDescription')
?? $page->getProduct()->getTranslation('description');
$metaInformation->setMetaDescription((string) $metaDescription);
$metaInformation->setMetaKeywords((string) $page->getProduct()->getTranslation('keywords'));
if ((string) $page->getProduct()->getTranslation('metaTitle') !== '') {
$metaInformation->setMetaTitle((string) $page->getProduct()->getTranslation('metaTitle'));
return;
}
$metaTitleParts = [$page->getProduct()->getTranslation('name')];
foreach ($page->getSelectedOptions() as $option) {
$metaTitleParts[] = $option->getTranslation('name');
}
$metaTitleParts[] = $page->getProduct()->getProductNumber();
$metaInformation->setMetaTitle(implode(' | ', $metaTitleParts));
}
private function loadDefaultAdditions(SalesChannelProductEntity $product, ProductPage $page, Request $request, SalesChannelContext $context): void
{
if ($cmsPage = $product->getCmsPage()) {
$page->setCmsPage($cmsPage);
return;
}
$request->request->set('parentId', $product->getParentId());
$reviews = $this->productReviewLoader->load($request, $context);
$reviews->setParentId($product->getParentId() ?? $product->getId());
$page->setReviews($reviews);
$crossSellings = $this->crossSellingRoute->load($product->getId(), new Request(), $context, new Criteria());
$page->setCrossSellings($crossSellings->getResult());
}
}