<?php declare(strict_types=1);
namespace Shopware\Storefront\Framework\Routing\NotFound;
use Shopware\Core\Framework\Adapter\Cache\AbstractCacheTracer;
use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
use Shopware\Core\Framework\Feature;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\PlatformRequest;
use Shopware\Core\SalesChannelRequest;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceInterface;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceParameters;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
use Shopware\Storefront\Controller\ErrorController;
use Shopware\Storefront\Framework\Routing\StorefrontResponse;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class NotFoundSubscriber implements EventSubscriberInterface
{
private const ALL_TAG = 'error-page';
private const SYSTEM_CONFIG_KEY = 'core.basicInformation.http404Page';
private ErrorController $controller;
private RequestStack $requestStack;
private SalesChannelContextServiceInterface $contextService;
private bool $kernelDebug;
private CacheInterface $cache;
/**
* @var AbstractCacheTracer<Response>
*/
private AbstractCacheTracer $cacheTracer;
private EntityCacheKeyGenerator $generator;
private CacheInvalidator $cacheInvalidator;
private EventDispatcherInterface $eventDispatcher;
/**
* @internal
*
* @param AbstractCacheTracer<Response> $cacheTracer
*/
public function __construct(
ErrorController $controller,
RequestStack $requestStack,
SalesChannelContextServiceInterface $contextService,
bool $kernelDebug,
CacheInterface $cache,
AbstractCacheTracer $cacheTracer,
EntityCacheKeyGenerator $generator,
CacheInvalidator $cacheInvalidator,
EventDispatcherInterface $eventDispatcher
) {
$this->controller = $controller;
$this->requestStack = $requestStack;
$this->contextService = $contextService;
$this->kernelDebug = $kernelDebug;
$this->cache = $cache;
$this->cacheTracer = $cacheTracer;
$this->generator = $generator;
$this->cacheInvalidator = $cacheInvalidator;
$this->eventDispatcher = $eventDispatcher;
}
public static function getSubscribedEvents(): array
{
if (Feature::isActive('v6.5.0.0')) {
return [
KernelEvents::EXCEPTION => [
['onError', -100],
],
SystemConfigChangedEvent::class => 'onSystemConfigChanged',
];
}
return [
SystemConfigChangedEvent::class => 'onSystemConfigChanged',
];
}
public function onError(ExceptionEvent $event): void
{
$request = $event->getRequest();
if ($this->kernelDebug || $request->attributes->has(SalesChannelRequest::ATTRIBUTE_STORE_API_PROXY)) {
return;
}
$event->stopPropagation();
$salesChannelId = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID, '');
$domainId = $request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_ID, '');
$languageId = $request->attributes->get(PlatformRequest::HEADER_LANGUAGE_ID, '');
if (!$request->attributes->has(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)) {
// When no sales-channel context is resolved, we need to resolve it now.
$this->setSalesChannelContext($request);
}
$is404StatusCode = $event->getThrowable() instanceof HttpException && $event->getThrowable()->getStatusCode() === Response::HTTP_NOT_FOUND;
// If the exception is not a 404 status code, we don't need to cache it.
if (!$is404StatusCode) {
$event->setResponse($this->controller->error(
$event->getThrowable(),
$request,
$event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)
));
return;
}
/** @var SalesChannelContext $context */
$context = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
$name = self::buildName($salesChannelId, $domainId, $languageId);
$key = $this->generateKey($salesChannelId, $domainId, $languageId, $request, $context);
$response = $this->cache->get($key, function (ItemInterface $item) use ($event, $name, $context) {
/** @var StorefrontResponse $response */
$response = $this->cacheTracer->trace($name, function () use ($event) {
/** @var Request $request */
$request = $this->requestStack->getMainRequest();
return $this->controller->error(
$event->getThrowable(),
$request,
$event->getRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT)
);
});
$item->tag($this->generateTags($name, $event->getRequest(), $context));
$response->setData(null);
$response->setContext(null);
return $response;
});
$event->setResponse($response);
}
public function onSystemConfigChanged(SystemConfigChangedEvent $event): void
{
if ($event->getKey() !== self::SYSTEM_CONFIG_KEY) {
return;
}
$this->cacheInvalidator->invalidate([self::ALL_TAG]);
}
public static function buildName(string $salesChannelId, string $domainId, string $languageId): string
{
return 'error-page-' . $salesChannelId . $domainId . $languageId;
}
private function generateKey(string $salesChannelId, string $domainId, string $languageId, Request $request, SalesChannelContext $context): string
{
$key = self::buildName($salesChannelId, $domainId, $languageId) . md5($this->generator->getSalesChannelContextHash($context));
$event = new NotFoundPageCacheKeyEvent($key, $request, $context);
$this->eventDispatcher->dispatch($event);
return $event->getKey();
}
private function generateTags(string $name, Request $request, SalesChannelContext $context): array
{
$tags = array_merge(
$this->cacheTracer->get($name),
[$name, self::ALL_TAG]
);
$event = new NotFoundPageTagsEvent($tags, $request, $context);
$this->eventDispatcher->dispatch($event);
return array_unique(array_filter($event->getTags()));
}
private function setSalesChannelContext(Request $request): void
{
$salesChannelId = (string) $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID);
$context = $this->contextService->get(
new SalesChannelContextServiceParameters(
$salesChannelId,
Uuid::randomHex(),
$request->headers->get(PlatformRequest::HEADER_LANGUAGE_ID),
$request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_CURRENCY_ID),
$request->attributes->get(SalesChannelRequest::ATTRIBUTE_DOMAIN_ID)
)
);
$request->attributes->set(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, $context);
}
}