<?php declare(strict_types=1);
namespace CioProductDownloadsTab\Subscriber;
use CioProductDownloadsTab\Entity\ProductDownloadDefinition;
use Shopware\Core\Content\Media\MediaEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class ProductPageSubscriber implements EventSubscriberInterface
{
private EntityRepository $productDownloadRepository;
public function __construct(EntityRepository $productDownloadRepository)
{
$this->productDownloadRepository = $productDownloadRepository;
}
public static function getSubscribedEvents(): array
{
return [
// CIO-AI-Driven: Storefront product detail page event (required for Twig page.extensions.*)
ProductPageLoadedEvent::class => 'onProductPageLoaded',
];
}
public function onProductPageLoaded(ProductPageLoadedEvent $event): void
{
$page = $event->getPage();
$product = $page->getProduct();
$salesChannelContext = $event->getSalesChannelContext();
$downloads = $this->getProductDownloadsByType($product->getId(), $salesChannelContext, ProductDownloadDefinition::TYPE_DOWNLOAD);
if ($downloads !== []) {
// CIO-AI-Driven: keep access consistent across PDP (page.extensions.*) and listing (product.extensions.*)
$page->addExtension('productDownloads', new ArrayStruct([
'downloads' => $downloads,
]));
$product->addExtension('productDownloads', new ArrayStruct([
'downloads' => $downloads,
]));
}
$cadDownloads = $this->getProductDownloadsByType($product->getId(), $salesChannelContext, ProductDownloadDefinition::TYPE_CAD);
if ($cadDownloads !== []) {
// CIO-AI-Driven: expose CAD downloads in a dedicated extension so Twig can render a separate tab
$page->addExtension('productCadDownloads', new ArrayStruct([
'downloads' => $cadDownloads,
]));
$product->addExtension('productCadDownloads', new ArrayStruct([
'downloads' => $cadDownloads,
]));
}
}
/**
* @return array<int, array{displayName: string, fileName: string, url: string|null}>
*/
private function getProductDownloadsByType(string $productId, SalesChannelContext $context, string $type): array
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $productId));
// CIO-AI-Driven: discriminator
$criteria->addFilter(new EqualsFilter('type', $type));
// CIO-AI-Driven: per-language downloads, use the current storefront request language
$criteria->addFilter(new EqualsFilter('languageId', $context->getContext()->getLanguageId()));
// CIO-AI-Driven: Admin stores via media_id now, so load the media association for a public URL
$criteria->addAssociation('media');
$result = $this->productDownloadRepository->search($criteria, $context->getContext());
$downloads = [];
foreach ($result->getEntities() as $download) {
$displayName = (string) $download->get('displayName');
$fileName = (string) $download->get('fileName');
$media = $download->get('media');
$url = null;
if ($media instanceof MediaEntity) {
$url = $media->getUrl();
$fileName = $fileName !== '' ? $fileName : $this->buildFileNameFromMedia($media);
$displayName = $displayName !== '' ? $displayName : $fileName;
}
// Backward compatibility (old implementations could have filePath)
if ($url === null) {
$url = $download->get('filePath') ? (string) $download->get('filePath') : null;
}
$downloads[] = [
'displayName' => $displayName,
'fileName' => $fileName,
'url' => $url,
];
}
return $downloads;
}
private function buildFileNameFromMedia(MediaEntity $media): string
{
$baseName = (string) ($media->getFileName() ?? '');
$extension = (string) ($media->getFileExtension() ?? '');
if ($baseName === '') {
return '';
}
return $extension !== '' ? "{$baseName}.{$extension}" : $baseName;
}
}