src/Core/Content/ImportExport/DataAbstractionLayer/Serializer/Entity/MediaSerializer.php line 129

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\ImportExport\DataAbstractionLayer\Serializer\Entity;
  3. use Shopware\Core\Content\ImportExport\Exception\InvalidMediaUrlException;
  4. use Shopware\Core\Content\ImportExport\Exception\MediaDownloadException;
  5. use Shopware\Core\Content\ImportExport\Struct\Config;
  6. use Shopware\Core\Content\Media\Aggregate\MediaFolder\MediaFolderEntity;
  7. use Shopware\Core\Content\Media\File\FileSaver;
  8. use Shopware\Core\Content\Media\File\MediaFile;
  9. use Shopware\Core\Content\Media\MediaEvents;
  10. use Shopware\Core\Content\Media\MediaService;
  11. use Shopware\Core\Framework\Context;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Uuid\Uuid;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Contracts\Service\ResetInterface;
  22. /**
  23.  * @internal
  24.  */
  25. #[Package('core')]
  26. class MediaSerializer extends EntitySerializer implements EventSubscriberInterfaceResetInterface
  27. {
  28.     /**
  29.      * @var array<string, array{media: MediaFile, destination: string}>
  30.      */
  31.     private array $cacheMediaFiles = [];
  32.     /**
  33.      * @internal
  34.      */
  35.     public function __construct(
  36.         private readonly MediaService $mediaService,
  37.         private readonly FileSaver $fileSaver,
  38.         private readonly EntityRepository $mediaFolderRepository,
  39.         private readonly EntityRepository $mediaRepository
  40.     ) {
  41.     }
  42.     /**
  43.      * @param array<mixed>|\Traversable<mixed> $entity
  44.      *
  45.      * @return array<mixed>|\Traversable<mixed>
  46.      */
  47.     public function deserialize(Config $configEntityDefinition $definition$entity)
  48.     {
  49.         $entity \is_array($entity) ? $entity iterator_to_array($entity);
  50.         $deserialized parent::deserialize($config$definition$entity);
  51.         $deserialized \is_array($deserialized) ? $deserialized iterator_to_array($deserialized);
  52.         $url $entity['url'] ?? null;
  53.         if (empty($url)) {
  54.             return $deserialized;
  55.         }
  56.         if (!filter_var($url\FILTER_VALIDATE_URL)) {
  57.             $deserialized['_error'] = new InvalidMediaUrlException($url);
  58.             return $deserialized;
  59.         }
  60.         $media null;
  61.         if (isset($deserialized['id'])) {
  62.             $media $this->mediaRepository->search(new Criteria([$deserialized['id']]), Context::createDefaultContext())->first();
  63.         }
  64.         $isNew $media === null;
  65.         if ($isNew || $media->getUrl() !== $url) {
  66.             $entityName $config->get('sourceEntity') ?? $definition->getEntityName();
  67.             $deserialized['mediaFolderId'] ??= $this->getMediaFolderId($deserialized['id'] ?? null$entityName);
  68.             $deserialized['id'] ??= Uuid::randomHex();
  69.             $parsed parse_url((string) $url);
  70.             if (!$parsed) {
  71.                 throw new \RuntimeException('Error parsing media URL: ' $url);
  72.             }
  73.             $pathInfo pathinfo($parsed['path'] ?? '');
  74.             $media $this->fetchFileFromURL((string) $url$pathInfo['extension'] ?? '');
  75.             if ($media === null) {
  76.                 $deserialized['_error'] = new MediaDownloadException($url);
  77.                 return $deserialized;
  78.             }
  79.             if ($isNew && $media->getHash()) {
  80.                 $deserialized $this->fetchExistingMediaByHash($deserialized$media->getHash());
  81.             }
  82.             $this->cacheMediaFiles[(string) $deserialized['id']] = [
  83.                 'media' => $media,
  84.                 'destination' => $pathInfo['filename'],
  85.             ];
  86.         }
  87.         return $deserialized;
  88.     }
  89.     public function supports(string $entity): bool
  90.     {
  91.         return $entity === 'media';
  92.     }
  93.     /**
  94.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  95.      */
  96.     public static function getSubscribedEvents(): array
  97.     {
  98.         return [
  99.             MediaEvents::MEDIA_WRITTEN_EVENT => 'persistMedia',
  100.         ];
  101.     }
  102.     /**
  103.      * @internal
  104.      */
  105.     public function persistMedia(EntityWrittenEvent $event): void
  106.     {
  107.         if (empty($this->cacheMediaFiles)) {
  108.             return;
  109.         }
  110.         $mediaFiles $this->cacheMediaFiles;
  111.         // prevent recursion
  112.         $this->cacheMediaFiles = [];
  113.         foreach ($event->getIds() as $id) {
  114.             if (!isset($mediaFiles[$id])) {
  115.                 continue;
  116.             }
  117.             $mediaFile $mediaFiles[$id];
  118.             $this->fileSaver->persistFileToMedia(
  119.                 $mediaFile['media'],
  120.                 $mediaFile['destination'],
  121.                 $id,
  122.                 $event->getContext()
  123.             );
  124.         }
  125.     }
  126.     public function reset(): void
  127.     {
  128.         $this->cacheMediaFiles = [];
  129.     }
  130.     private function getMediaFolderId(?string $idstring $entity): string
  131.     {
  132.         if ($id !== null) {
  133.             /** @var MediaFolderEntity|null $folder */
  134.             $folder $this->mediaFolderRepository->search(new Criteria([$id]), Context::createDefaultContext())->first();
  135.             if ($folder !== null) {
  136.                 return $folder->getId();
  137.             }
  138.         }
  139.         $criteria = new Criteria();
  140.         $criteria->addFilter(new EqualsFilter('media_folder.defaultFolder.entity'$entity));
  141.         $criteria->addAssociation('defaultFolder');
  142.         /** @var MediaFolderEntity|null $default */
  143.         $default $this->mediaFolderRepository->search($criteriaContext::createDefaultContext())->first();
  144.         if ($default !== null) {
  145.             return $default->getId();
  146.         }
  147.         $criteria = new Criteria();
  148.         $criteria->addFilter(new EqualsFilter('media_folder.defaultFolder.entity''import_export_profile'));
  149.         $criteria->addAssociation('defaultFolder');
  150.         /** @var MediaFolderEntity|null $fallback */
  151.         $fallback $this->mediaFolderRepository->search($criteriaContext::createDefaultContext())->first();
  152.         if ($fallback === null) {
  153.             throw new \RuntimeException('Failed to find default media folder for import_export_profile');
  154.         }
  155.         return $fallback->getId();
  156.     }
  157.     private function fetchFileFromURL(string $urlstring $extension): ?MediaFile
  158.     {
  159.         $request = new Request();
  160.         $request->query->set('url'$url);
  161.         $request->query->set('extension'$extension);
  162.         $request->request->set('url'$url);
  163.         $request->request->set('extension'$extension);
  164.         $request->headers->set('content-type''application/json');
  165.         try {
  166.             $file $this->mediaService->fetchFile($request);
  167.             if ($file !== null && $file->getFileSize() > 0) {
  168.                 return $file;
  169.             }
  170.         } catch (\Throwable) {
  171.         }
  172.         return null;
  173.     }
  174.     /**
  175.      * @param array<string, mixed> $deserialized
  176.      *
  177.      * @return array<string, mixed>
  178.      */
  179.     private function fetchExistingMediaByHash(array $deserializedstring $hash): array
  180.     {
  181.         $criteria = new Criteria();
  182.         $criteria->addFilter(new EqualsFilter('metaData.hash'$hash));
  183.         $media $this->mediaRepository->search($criteriaContext::createDefaultContext())->first();
  184.         if ($media) {
  185.             $deserialized['id'] = $media->getId();
  186.         }
  187.         return $deserialized;
  188.     }
  189. }