custom/plugins/P2LabProductVideo/src/Subscriber/MediaSubscriber.php line 442

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace P2Lab\ProductVideo\Subscriber;
  3. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  4. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  7. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  8. use Shopware\Core\Content\Media\MediaEvents;
  9. use Shopware\Core\Content\Product\ProductEvents;
  10. use Doctrine\DBAL\Connection;
  11. use Doctrine\DBAL\Query\QueryBuilder;
  12. use P2Lab\ProductVideo\Helper\EmbedVideoHelper;
  13. use Shopware\Core\Content\Media\File\FileFetcher;
  14. use Shopware\Core\Content\Media\File\FileSaver;
  15. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  18. use Shopware\Core\System\SystemConfig\SystemConfigService;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\CountResult;
  20. use Psr\Log\LoggerInterface;
  21. use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaEntity;
  22. use Shopware\Core\Defaults;
  23. use Shopware\Core\Framework\Uuid\Uuid;
  24. class MediaSubscriber implements EventSubscriberInterface
  25. {
  26.     private Connection $connection;
  27.     private EntityRepositoryInterface $productRepository;
  28.     /** @var EntityRepositoryInterface */
  29.     private $mediaRepository;
  30.     /** @var EntityRepositoryInterface */
  31.     private EntityRepositoryInterface $productMediaRepository;
  32.     /** @var FileFetcher */
  33.     private FileFetcher $fileFetcher;
  34.     /** @var FileSaver */
  35.     private FileSaver $fileSaver;
  36.     /** @var EventDispatcherInterface */
  37.     private EventDispatcherInterface $eventDispatcher;
  38.     /** @var SystemConfigService */
  39.     private SystemConfigService $systemConfigService;
  40.     /** @var LoggerInterface */
  41.     private $logger;
  42.     /** @var bool */
  43.     private bool $isBulkApiEnabled false;
  44.     /** @var bool */
  45.     private static bool $isActiveEventOnProductWritten true;
  46.     /**
  47.      * @internal
  48.      */
  49.     public function __construct(
  50.         Connection $connection,
  51.         EntityRepositoryInterface $productRepository,
  52.         $mediaRepository,
  53.         EntityRepositoryInterface $productMediaRepository,
  54.         FileFetcher $fileFetcher,
  55.         FileSaver $fileSaver,
  56.         EventDispatcherInterface $eventDispatcher,
  57.         SystemConfigService $systemConfigService,
  58.         LoggerInterface $logger
  59.     )
  60.     {
  61.         $this->connection $connection;
  62.         $this->productRepository $productRepository;
  63.         $this->mediaRepository $mediaRepository;
  64.         $this->productMediaRepository $productMediaRepository;
  65.         $this->fileFetcher $fileFetcher;
  66.         $this->fileSaver $fileSaver;
  67.         $this->eventDispatcher $eventDispatcher;
  68.         $this->systemConfigService $systemConfigService;
  69.         $this->logger $logger;
  70.         $this->isBulkApiEnabled = (bool) $this->systemConfigService->get('P2LabProductVideo.config.bulkApiEnabled');
  71.     }
  72.     /**
  73.      * @return array
  74.      */
  75.     public static function getSubscribedEvents(): array
  76.     {
  77.         return [
  78.             MediaEvents::MEDIA_DELETED_EVENT => 'onMediaDeleted',
  79.             ProductEvents::PRODUCT_WRITTEN_EVENT => 'onProductWritten',
  80.         ];
  81.     }
  82.     
  83.     /**
  84.      * @param array<mixed> $array
  85.      */
  86.     private static function isCollection(array $array): bool
  87.     {
  88.         return array_keys($array) === range(0\count($array) - 1);
  89.     }
  90.     /**
  91.      * @param array $productIds
  92.      * @return array
  93.      */
  94.     private function getWrittenProducts(array $productIds): array
  95.     {
  96.         if (! $productIds) return [];
  97.         /** @var QueryBuilder $query */
  98.         $query = new QueryBuilder($this->connection);
  99.         
  100.         $query
  101.             ->select('product_translation.*')
  102.             ->from('product_translation')
  103.             ->where('product_translation.product_id IN (:productIds)')
  104.             ->where('product_translation.product_version_id = :productVersionId')
  105.             ->andWhere("JSON_EXTRACT(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL")
  106.             ->setParameter('productIds'array_map(function($id){
  107.                 return Uuid::fromHexToBytes($id);
  108.             }, $productIds), Connection::PARAM_STR_ARRAY)
  109.             ->setParameter('productVersionId'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  110.         /** @var array $products */
  111.         $products = [];
  112.         /** @var array $rows */
  113.         $rows $query->execute()->fetchAllAssociative();
  114.         /** @var array $row */
  115.         foreach ($rows as $row) {
  116.             /** @var array $customFields */
  117.             $customFields json_decode$row['custom_fields'], true );
  118.             if (isset($customFields['p2labProductVideo'])) {
  119.                 /** @var string $productId */
  120.                 $productId Uuid::fromBytesToHex($row['product_id']);
  121.                 /** @var array $p2labProductVideo */
  122.                 $p2labProductVideo $customFields['p2labProductVideo'];
  123.                 if (!self::isCollection($p2labProductVideo)) {
  124.                     $p2labProductVideo = [ $p2labProductVideo ];
  125.                 }
  126.                 foreach ($p2labProductVideo as $video) {
  127.                     $products[$productId][] = $video;
  128.                 }
  129.             }
  130.         }
  131.         $this->connection->executeStatement("
  132.             UPDATE 
  133.                 `product_translation` 
  134.             SET
  135.                 `product_translation`.`custom_fields` = JSON_REMOVE(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"')
  136.             WHERE 
  137.                 `product_translation`.`product_id` IN (:productIds)
  138.                     AND
  139.                 `product_translation`.`product_version_id` = :productVersionId
  140.                     AND
  141.                 JSON_EXTRACT(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL
  142.         ", [
  143.             'productIds' => array_map(function($id){
  144.                 return Uuid::fromHexToBytes($id);
  145.             }, $productIds),
  146.             'productVersionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  147.         ], [
  148.             'productIds' => Connection::PARAM_STR_ARRAY,
  149.         ]);
  150.         $this->connection->executeStatement("
  151.             UPDATE 
  152.                 `product_translation` 
  153.             SET
  154.                 `product_translation`.`custom_fields` = NULL
  155.             WHERE 
  156.                 `product_translation`.`product_id` IN (:productIds)
  157.                     AND
  158.                 `product_translation`.`product_version_id` = :productVersionId
  159.                     AND
  160.                 JSON_LENGTH(custom_fields) = 0
  161.         ", [
  162.             'productIds' => array_map(function($id){
  163.                 return Uuid::fromHexToBytes($id);
  164.             }, $productIds),
  165.             'productVersionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
  166.         ], [
  167.             'productIds' => Connection::PARAM_STR_ARRAY,
  168.         ]);
  169.         return $products;
  170.     }
  171.     /**
  172.      * @param array $productIds
  173.      * @return array
  174.      */
  175.     private function getProductsMediaUrls(array $productIds): array
  176.     {
  177.         if (! $productIds) return [];
  178.         /** @var QueryBuilder $query */
  179.         $query = new QueryBuilder($this->connection);
  180.         
  181.         $query
  182.             ->select('product_media.*')
  183.             ->from('product_media')
  184.             ->where('product_media.product_id IN (:productIds)')
  185.             ->andWhere("JSON_EXTRACT(`product_media`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL")
  186.             ->setParameter('productIds'array_map(function($id){
  187.                 return hex2bin($id);
  188.             }, $productIds), Connection::PARAM_STR_ARRAY);
  189.         /** @var array $productMediaUrls */
  190.         $productMediaUrls = [];
  191.         /** @var array $rows */
  192.         $rows $query->execute()->fetchAllAssociative();
  193.         /** @var array $row */
  194.         foreach ($rows as $row) {
  195.             /** @var array $customFields */
  196.             $customFields json_decode$row['custom_fields'], true );
  197.             /** @var array $p2labProductVideo */
  198.             $p2labProductVideo $customFields['p2labProductVideo'];
  199.             if (! empty($p2labProductVideo['url'])) {
  200.                 $productMediaUrls[Uuid::fromBytesToHex($row['product_id'])][] = $p2labProductVideo['url'];
  201.             }
  202.         }
  203.         return $productMediaUrls;
  204.     }
  205.     /**
  206.      * @param EntityWrittenEvent $event
  207.      */
  208.     public function onProductWritten(EntityWrittenEvent $event): void
  209.     {
  210.         if (! $this->isBulkApiEnabled) {
  211.             return;
  212.         }
  213.         if ($event->getContext()->getVersionId() != Defaults::LIVE_VERSION) {
  214.             return;
  215.         }
  216.         
  217.         /** @var array $productIds */
  218.         if (null == ($productIds $event->getIds())) return;
  219.         
  220.         if (! self::$isActiveEventOnProductWritten) return;
  221.         self::$isActiveEventOnProductWritten false;
  222.         
  223.         /** @var array $products */
  224.         $products $this->getWrittenProducts($productIds);
  225.         /** @var array $productsMediaUrls */
  226.         $productsMediaUrls $this->getProductsMediaUrls($productIds);
  227.         /** @var array $videos */
  228.         foreach ($products as $productId => $videos) {
  229.             /** @var array $video */
  230.             foreach ($videos as $video) {
  231.                 if (empty($video['autoThumbnail'])) {
  232.                     if (empty($video['mediaId'])) {
  233.                         $this->logger->error(sprintf(
  234.                             "The product with ID '%s' has no 'p2labProductVideo.mediaId' or 'p2labProductVideo.autoThumbnail'."$productId)
  235.                         );
  236.                         continue;
  237.                     }
  238.                 }
  239.                 if (empty($video['url'])) {
  240.                     $this->logger->error(sprintf(
  241.                         "The product with ID '%s' has no 'p2labProductVideo.url'."$productId)
  242.                     );
  243.                     continue;
  244.                 }
  245.                             
  246.                 if (!EmbedVideoHelper::recognizeEmbedVideoType($video['url'])) {
  247.                     $this->logger->error(sprintf(
  248.                         "The video source for the product with ID '%s' of '%s' is not recognized."$productId$video['url'])
  249.                     );
  250.                             
  251.                     continue;
  252.                 }
  253.                 if (isset($productsMediaUrls[$productId])) {
  254.                     /** @var string $url */
  255.                     $url EmbedVideoHelper::normalizeEmbedVideoUrl($video['url']);
  256.                     if (in_array($url$productsMediaUrls[$productId])) {
  257.                         $this->logger->debug(sprintf(
  258.                             'The video source "%s" already exists in the product with ID "%s" and has been skipped.'
  259.                             $url
  260.                             $productId
  261.                         ));
  262.                         continue;
  263.                     }
  264.                 }
  265.                 /** @var array $data */
  266.                 $data EmbedVideoHelper::upload(
  267.                     $this->mediaRepository,
  268.                     $this->fileFetcher,
  269.                     $this->fileSaver,
  270.                     $this->eventDispatcher,
  271.                     [
  272.                         'productId' => $productId,
  273.                         'mediaId' => empty($video['mediaId']) ? null $video['mediaId'],
  274.                         'customFields' => [
  275.                             'p2labProductVideo' => [
  276.                                 'url' => $video['url'],
  277.                             ],
  278.                         ],                    
  279.                     ], 
  280.                     $event->getContext(),
  281.                     null,
  282.                     $this->logger
  283.                 );
  284.                         
  285.                 if (empty($data['mediaId'])) {
  286.                     $this->logger->error(sprintf(
  287.                         "The video source for the product with ID '%s' of '%s' could not be uploaded."$productId$video['url'])
  288.                     );
  289.                     continue;
  290.                 }
  291.                 /** @var Criteria $criteria */
  292.                 $criteria = new Criteria();
  293.                 $criteria->addFilter(new EqualsFilter('productId'$productId));
  294.                 $criteria->addAggregation(new CountAggregation('product-media-count''id'));
  295.                 /** @var CountResult $productMediaCount */
  296.                 $productMediaCount $this->productMediaRepository->search($criteria$event->getContext())
  297.                     ->getAggregations()->get('product-media-count');
  298.                 $data['position'] = $productMediaCount->getCount();
  299.                 $this->productMediaRepository->create([
  300.                     $data
  301.                 ], $event->getContext());
  302.                 $productsMediaUrls[$productId][] = EmbedVideoHelper::normalizeEmbedVideoUrl($video['url']);
  303.                 // Set coverId if added media is the first one
  304.                 if (! $data['position']) {
  305.                     /** @var Criteria $criteria */
  306.                     $criteria = new Criteria();
  307.                     $criteria->addFilter(new EqualsFilter('productId'$productId));
  308.                     $criteria->setLimit(1);
  309.                     /** @var ProductMediaEntity $productMedia */
  310.                     if (null != ($productMedia $this->productMediaRepository->search($criteria$event->getContext())->first())) {
  311.                         $this->productRepository->update([
  312.                             [
  313.                                 'id' => $productId,
  314.                                 'coverId' => $productMedia->getId(),
  315.                             ]
  316.                         ], $event->getContext());
  317.                     }
  318.                 }
  319.             }
  320.         }
  321.         self::$isActiveEventOnProductWritten true;
  322.     }
  323.     /**
  324.      * @param array $mediaIds
  325.      */
  326.     public function deleteRelatedMedia(array $mediaIds): void
  327.     {
  328.         $this->connection->executeStatement('
  329.             DELETE FROM 
  330.                 `product_media`
  331.             WHERE 
  332.                 JSON_EXTRACT(`custom_fields`, "$.p2labProductVideo.mediaId") IN (:mediaIds)
  333.             ',
  334.             ['mediaIds' => $mediaIds],
  335.             ['mediaIds' => Connection::PARAM_STR_ARRAY]
  336.         );
  337.     }
  338.     /**
  339.      * @param array $mediaIds
  340.      */
  341.     public function deleteRelatedTranslations(array $mediaIds): void
  342.     {
  343.         /** @var array $conditions */
  344.         $conditions array_map(function($mediaId){
  345.             return "JSON_SEARCH(`custom_fields`, 'one', " $this->connection->quote($mediaId) . ", NULL, \"$.p2labProductVideo.translation.*\") IS NOT NULL";
  346.         }, $mediaIds);
  347.         /** @var string $sql */
  348.         $sql sprintf('
  349.             SELECT 
  350.                 *
  351.             FROM
  352.                 `product_media`
  353.             WHERE 
  354.                 %s
  355.         'implode(' OR '$conditions));
  356.         /** @var array $rows */
  357.         $rows $this->connection->fetchAllAssociative($sql);
  358.         /** @var array $row */
  359.         foreach ($rows as $row) {
  360.             /** @var array $customFields */
  361.             $customFields json_decode$row['custom_fields'], true );
  362.             $customFields['p2labProductVideo']['translation'] = array_filter($customFields['p2labProductVideo']['translation'], function($mediaId) use($mediaIds){
  363.                 return ! in_array($mediaId$mediaIds);
  364.             });
  365.             $this->connection->update('product_media', [
  366.                 'custom_fields' => json_encode($customFields),
  367.             ], [
  368.                 'id' => $row['id'],
  369.             ]);
  370.         }
  371.     }
  372.     /**
  373.      * @param EntityDeletedEvent $event
  374.      * @return bool
  375.      */
  376.     public function onMediaDeleted(EntityDeletedEvent $event): bool
  377.     {
  378.         /** @var array $mediaIds */
  379.         $mediaIds $event->getIds();
  380.         $this->deleteRelatedMedia($mediaIds);
  381.         $this->deleteRelatedTranslations($mediaIds);
  382.         return true;
  383.     }
  384. }