<?php declare(strict_types=1);
namespace Shopware\Storefront\Controller;
use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
use Shopware\Core\Content\Product\Exception\ReviewNotActiveExeption;
use Shopware\Core\Content\Product\Exception\VariantNotFoundException;
use Shopware\Core\Content\Product\SalesChannel\FindVariant\FindProductVariantRoute;
use Shopware\Core\Content\Product\SalesChannel\Review\AbstractProductReviewSaveRoute;
use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
use Shopware\Core\Framework\Feature;
use Shopware\Core\Framework\Routing\Annotation\LoginRequired;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Core\Framework\Routing\Annotation\Since;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\Framework\Validation\Exception\ConstraintViolationException;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Framework\Cache\Annotation\HttpCache;
use Shopware\Storefront\Framework\Routing\RequestTransformer;
use Shopware\Storefront\Page\Product\Configurator\ProductCombinationFinder;
use Shopware\Storefront\Page\Product\ProductPageLoadedHook;
use Shopware\Storefront\Page\Product\ProductPageLoader;
use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageLoader;
use Shopware\Storefront\Page\Product\QuickView\ProductQuickViewWidgetLoadedHook;
use Shopware\Storefront\Page\Product\Review\ProductReviewLoader;
use Shopware\Storefront\Page\Product\Review\ProductReviewsWidgetLoadedHook;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route(defaults={"_routeScope"={"storefront"}})
*
* @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal
*/
class ProductController extends StorefrontController
{
private ProductPageLoader $productPageLoader;
private FindProductVariantRoute $findVariantRoute;
private MinimalQuickViewPageLoader $minimalQuickViewPageLoader;
private SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler;
private ProductReviewLoader $productReviewLoader;
private SystemConfigService $systemConfigService;
private AbstractProductReviewSaveRoute $productReviewSaveRoute;
/**
* @deprecated tag:v6.5.0 - will be removed
*/
private ProductCombinationFinder $productCombinationFinder;
/**
* @internal
*/
public function __construct(
ProductPageLoader $productPageLoader,
ProductCombinationFinder $productCombinationFinder,
FindProductVariantRoute $findVariantRoute,
MinimalQuickViewPageLoader $minimalQuickViewPageLoader,
AbstractProductReviewSaveRoute $productReviewSaveRoute,
SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler,
ProductReviewLoader $productReviewLoader,
SystemConfigService $systemConfigService
) {
$this->productPageLoader = $productPageLoader;
$this->findVariantRoute = $findVariantRoute;
$this->minimalQuickViewPageLoader = $minimalQuickViewPageLoader;
$this->seoUrlPlaceholderHandler = $seoUrlPlaceholderHandler;
$this->productReviewLoader = $productReviewLoader;
$this->systemConfigService = $systemConfigService;
$this->productReviewSaveRoute = $productReviewSaveRoute;
$this->productCombinationFinder = $productCombinationFinder;
}
/**
* @Since("6.3.3.0")
* @HttpCache()
* @Route("/detail/{productId}", name="frontend.detail.page", methods={"GET"})
*/
public function index(SalesChannelContext $context, Request $request): Response
{
$page = $this->productPageLoader->load($request, $context);
$this->hook(new ProductPageLoadedHook($page, $context));
$ratingSuccess = $request->get('success');
/**
* @deprecated tag:v6.5.0 - remove complete if statement, cms page id is always set
*
* Fallback layout for non-assigned product layout
*/
if (!$page->getCmsPage()) {
Feature::throwException('v6.5.0.0', 'Fallback will be removed because cms page is always set in subscriber.');
return $this->renderStorefront('@Storefront/storefront/page/product-detail/index.html.twig', ['page' => $page, 'ratingSuccess' => $ratingSuccess]);
}
return $this->renderStorefront('@Storefront/storefront/page/content/product-detail.html.twig', ['page' => $page]);
}
/**
* @Since("6.0.0.0")
* @HttpCache()
* @Route("/detail/{productId}/switch", name="frontend.detail.switch", methods={"GET"}, defaults={"XmlHttpRequest": true})
*/
public function switch(string $productId, Request $request, SalesChannelContext $salesChannelContext): JsonResponse
{
$switchedGroup = $request->query->has('switched') ? (string) $request->query->get('switched') : null;
/** @var array<mixed>|null $options */
$options = json_decode($request->query->get('options', ''), true);
try {
if (Feature::isActive('v6.5.0.0')) {
$variantResponse = $this->findVariantRoute->load(
$productId,
new Request(
[
'switchedGroup' => $switchedGroup,
'options' => $options ?? [],
]
),
$salesChannelContext
);
$productId = $variantResponse->getFoundCombination()->getVariantId();
} else {
$finderResponse = $this->productCombinationFinder->find(
$productId,
$switchedGroup,
$options ?? [],
$salesChannelContext
);
$productId = $finderResponse->getVariantId();
}
} catch (VariantNotFoundException|ProductNotFoundException $productNotFoundException) {
//nth
}
$host = $request->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
. $request->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
$url = $this->seoUrlPlaceholderHandler->replace(
$this->seoUrlPlaceholderHandler->generate(
'frontend.detail.page',
['productId' => $productId]
),
$host,
$salesChannelContext
);
$response = new JsonResponse([
'url' => $url,
'productId' => $productId,
]);
$response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, '1');
return $response;
}
/**
* @Since("6.0.0.0")
* @Route("/quickview/{productId}", name="widgets.quickview.minimal", methods={"GET"}, defaults={"XmlHttpRequest": true})
*/
public function quickviewMinimal(Request $request, SalesChannelContext $context): Response
{
$page = $this->minimalQuickViewPageLoader->load($request, $context);
$this->hook(new ProductQuickViewWidgetLoadedHook($page, $context));
return $this->renderStorefront('@Storefront/storefront/component/product/quickview/minimal.html.twig', ['page' => $page]);
}
/**
* @Since("6.0.0.0")
* @Route("/product/{productId}/rating", name="frontend.detail.review.save", methods={"POST"}, defaults={"XmlHttpRequest"=true, "_loginRequired"=true})
*/
public function saveReview(string $productId, RequestDataBag $data, SalesChannelContext $context): Response
{
$this->checkReviewsActive($context);
try {
$this->productReviewSaveRoute->save($productId, $data, $context);
} catch (ConstraintViolationException $formViolations) {
return $this->forwardToRoute('frontend.product.reviews', [
'productId' => $productId,
'success' => -1,
'formViolations' => $formViolations,
'data' => $data,
], ['productId' => $productId]);
}
$forwardParams = [
'productId' => $productId,
'success' => 1,
'data' => $data,
'parentId' => $data->get('parentId'),
];
if ($data->has('id')) {
$forwardParams['success'] = 2;
}
return $this->forwardToRoute('frontend.product.reviews', $forwardParams, ['productId' => $productId]);
}
/**
* @Since("6.0.0.0")
* @Route("/product/{productId}/reviews", name="frontend.product.reviews", methods={"GET","POST"}, defaults={"XmlHttpRequest"=true})
*/
public function loadReviews(Request $request, RequestDataBag $data, SalesChannelContext $context): Response
{
$this->checkReviewsActive($context);
$reviews = $this->productReviewLoader->load($request, $context);
$this->hook(new ProductReviewsWidgetLoadedHook($reviews, $context));
return $this->renderStorefront('storefront/page/product-detail/review/review.html.twig', [
'reviews' => $reviews,
'ratingSuccess' => $request->get('success'),
]);
}
/**
* @throws ReviewNotActiveExeption
*/
private function checkReviewsActive(SalesChannelContext $context): void
{
$showReview = $this->systemConfigService->get('core.listing.showReview', $context->getSalesChannel()->getId());
if (!$showReview) {
throw new ReviewNotActiveExeption();
}
}
}