<?php declare(strict_types=1);
namespace P2Lab\ProductVideo\Subscriber;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Content\Media\MediaEvents;
use Shopware\Core\Content\Product\ProductEvents;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
use P2Lab\ProductVideo\Helper\EmbedVideoHelper;
use Shopware\Core\Content\Media\File\FileFetcher;
use Shopware\Core\Content\Media\File\FileSaver;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\Metric\CountResult;
use Psr\Log\LoggerInterface;
use Shopware\Core\Content\Product\Aggregate\ProductMedia\ProductMediaEntity;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Uuid\Uuid;
class MediaSubscriber implements EventSubscriberInterface
{
private Connection $connection;
private EntityRepositoryInterface $productRepository;
/** @var EntityRepositoryInterface */
private $mediaRepository;
/** @var EntityRepositoryInterface */
private EntityRepositoryInterface $productMediaRepository;
/** @var FileFetcher */
private FileFetcher $fileFetcher;
/** @var FileSaver */
private FileSaver $fileSaver;
/** @var EventDispatcherInterface */
private EventDispatcherInterface $eventDispatcher;
/** @var SystemConfigService */
private SystemConfigService $systemConfigService;
/** @var LoggerInterface */
private $logger;
/** @var bool */
private bool $isBulkApiEnabled = false;
/** @var bool */
private static bool $isActiveEventOnProductWritten = true;
/**
* @internal
*/
public function __construct(
Connection $connection,
EntityRepositoryInterface $productRepository,
$mediaRepository,
EntityRepositoryInterface $productMediaRepository,
FileFetcher $fileFetcher,
FileSaver $fileSaver,
EventDispatcherInterface $eventDispatcher,
SystemConfigService $systemConfigService,
LoggerInterface $logger
)
{
$this->connection = $connection;
$this->productRepository = $productRepository;
$this->mediaRepository = $mediaRepository;
$this->productMediaRepository = $productMediaRepository;
$this->fileFetcher = $fileFetcher;
$this->fileSaver = $fileSaver;
$this->eventDispatcher = $eventDispatcher;
$this->systemConfigService = $systemConfigService;
$this->logger = $logger;
$this->isBulkApiEnabled = (bool) $this->systemConfigService->get('P2LabProductVideo.config.bulkApiEnabled');
}
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
MediaEvents::MEDIA_DELETED_EVENT => 'onMediaDeleted',
ProductEvents::PRODUCT_WRITTEN_EVENT => 'onProductWritten',
];
}
/**
* @param array<mixed> $array
*/
private static function isCollection(array $array): bool
{
return array_keys($array) === range(0, \count($array) - 1);
}
/**
* @param array $productIds
* @return array
*/
private function getWrittenProducts(array $productIds): array
{
if (! $productIds) return [];
/** @var QueryBuilder $query */
$query = new QueryBuilder($this->connection);
$query
->select('product_translation.*')
->from('product_translation')
->where('product_translation.product_id IN (:productIds)')
->where('product_translation.product_version_id = :productVersionId')
->andWhere("JSON_EXTRACT(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL")
->setParameter('productIds', array_map(function($id){
return Uuid::fromHexToBytes($id);
}, $productIds), Connection::PARAM_STR_ARRAY)
->setParameter('productVersionId', Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
/** @var array $products */
$products = [];
/** @var array $rows */
$rows = $query->execute()->fetchAllAssociative();
/** @var array $row */
foreach ($rows as $row) {
/** @var array $customFields */
$customFields = json_decode( $row['custom_fields'], true );
if (isset($customFields['p2labProductVideo'])) {
/** @var string $productId */
$productId = Uuid::fromBytesToHex($row['product_id']);
/** @var array $p2labProductVideo */
$p2labProductVideo = $customFields['p2labProductVideo'];
if (!self::isCollection($p2labProductVideo)) {
$p2labProductVideo = [ $p2labProductVideo ];
}
foreach ($p2labProductVideo as $video) {
$products[$productId][] = $video;
}
}
}
$this->connection->executeStatement("
UPDATE
`product_translation`
SET
`product_translation`.`custom_fields` = JSON_REMOVE(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"')
WHERE
`product_translation`.`product_id` IN (:productIds)
AND
`product_translation`.`product_version_id` = :productVersionId
AND
JSON_EXTRACT(`product_translation`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL
", [
'productIds' => array_map(function($id){
return Uuid::fromHexToBytes($id);
}, $productIds),
'productVersionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
], [
'productIds' => Connection::PARAM_STR_ARRAY,
]);
$this->connection->executeStatement("
UPDATE
`product_translation`
SET
`product_translation`.`custom_fields` = NULL
WHERE
`product_translation`.`product_id` IN (:productIds)
AND
`product_translation`.`product_version_id` = :productVersionId
AND
JSON_LENGTH(custom_fields) = 0
", [
'productIds' => array_map(function($id){
return Uuid::fromHexToBytes($id);
}, $productIds),
'productVersionId' => Uuid::fromHexToBytes(Defaults::LIVE_VERSION),
], [
'productIds' => Connection::PARAM_STR_ARRAY,
]);
return $products;
}
/**
* @param array $productIds
* @return array
*/
private function getProductsMediaUrls(array $productIds): array
{
if (! $productIds) return [];
/** @var QueryBuilder $query */
$query = new QueryBuilder($this->connection);
$query
->select('product_media.*')
->from('product_media')
->where('product_media.product_id IN (:productIds)')
->andWhere("JSON_EXTRACT(`product_media`.`custom_fields`, '$.\"p2labProductVideo\"') IS NOT NULL")
->setParameter('productIds', array_map(function($id){
return hex2bin($id);
}, $productIds), Connection::PARAM_STR_ARRAY);
/** @var array $productMediaUrls */
$productMediaUrls = [];
/** @var array $rows */
$rows = $query->execute()->fetchAllAssociative();
/** @var array $row */
foreach ($rows as $row) {
/** @var array $customFields */
$customFields = json_decode( $row['custom_fields'], true );
/** @var array $p2labProductVideo */
$p2labProductVideo = $customFields['p2labProductVideo'];
if (! empty($p2labProductVideo['url'])) {
$productMediaUrls[Uuid::fromBytesToHex($row['product_id'])][] = $p2labProductVideo['url'];
}
}
return $productMediaUrls;
}
/**
* @param EntityWrittenEvent $event
*/
public function onProductWritten(EntityWrittenEvent $event): void
{
if (! $this->isBulkApiEnabled) {
return;
}
if ($event->getContext()->getVersionId() != Defaults::LIVE_VERSION) {
return;
}
/** @var array $productIds */
if (null == ($productIds = $event->getIds())) return;
if (! self::$isActiveEventOnProductWritten) return;
self::$isActiveEventOnProductWritten = false;
/** @var array $products */
$products = $this->getWrittenProducts($productIds);
/** @var array $productsMediaUrls */
$productsMediaUrls = $this->getProductsMediaUrls($productIds);
/** @var array $videos */
foreach ($products as $productId => $videos) {
/** @var array $video */
foreach ($videos as $video) {
if (empty($video['autoThumbnail'])) {
if (empty($video['mediaId'])) {
$this->logger->error(sprintf(
"The product with ID '%s' has no 'p2labProductVideo.mediaId' or 'p2labProductVideo.autoThumbnail'.", $productId)
);
continue;
}
}
if (empty($video['url'])) {
$this->logger->error(sprintf(
"The product with ID '%s' has no 'p2labProductVideo.url'.", $productId)
);
continue;
}
if (!EmbedVideoHelper::recognizeEmbedVideoType($video['url'])) {
$this->logger->error(sprintf(
"The video source for the product with ID '%s' of '%s' is not recognized.", $productId, $video['url'])
);
continue;
}
if (isset($productsMediaUrls[$productId])) {
/** @var string $url */
$url = EmbedVideoHelper::normalizeEmbedVideoUrl($video['url']);
if (in_array($url, $productsMediaUrls[$productId])) {
$this->logger->debug(sprintf(
'The video source "%s" already exists in the product with ID "%s" and has been skipped.',
$url,
$productId
));
continue;
}
}
/** @var array $data */
$data = EmbedVideoHelper::upload(
$this->mediaRepository,
$this->fileFetcher,
$this->fileSaver,
$this->eventDispatcher,
[
'productId' => $productId,
'mediaId' => empty($video['mediaId']) ? null : $video['mediaId'],
'customFields' => [
'p2labProductVideo' => [
'url' => $video['url'],
],
],
],
$event->getContext(),
null,
$this->logger
);
if (empty($data['mediaId'])) {
$this->logger->error(sprintf(
"The video source for the product with ID '%s' of '%s' could not be uploaded.", $productId, $video['url'])
);
continue;
}
/** @var Criteria $criteria */
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $productId));
$criteria->addAggregation(new CountAggregation('product-media-count', 'id'));
/** @var CountResult $productMediaCount */
$productMediaCount = $this->productMediaRepository->search($criteria, $event->getContext())
->getAggregations()->get('product-media-count');
$data['position'] = $productMediaCount->getCount();
$this->productMediaRepository->create([
$data
], $event->getContext());
$productsMediaUrls[$productId][] = EmbedVideoHelper::normalizeEmbedVideoUrl($video['url']);
// Set coverId if added media is the first one
if (! $data['position']) {
/** @var Criteria $criteria */
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $productId));
$criteria->setLimit(1);
/** @var ProductMediaEntity $productMedia */
if (null != ($productMedia = $this->productMediaRepository->search($criteria, $event->getContext())->first())) {
$this->productRepository->update([
[
'id' => $productId,
'coverId' => $productMedia->getId(),
]
], $event->getContext());
}
}
}
}
self::$isActiveEventOnProductWritten = true;
}
/**
* @param array $mediaIds
*/
public function deleteRelatedMedia(array $mediaIds): void
{
$this->connection->executeStatement('
DELETE FROM
`product_media`
WHERE
JSON_EXTRACT(`custom_fields`, "$.p2labProductVideo.mediaId") IN (:mediaIds)
',
['mediaIds' => $mediaIds],
['mediaIds' => Connection::PARAM_STR_ARRAY]
);
}
/**
* @param array $mediaIds
*/
public function deleteRelatedTranslations(array $mediaIds): void
{
/** @var array $conditions */
$conditions = array_map(function($mediaId){
return "JSON_SEARCH(`custom_fields`, 'one', " . $this->connection->quote($mediaId) . ", NULL, \"$.p2labProductVideo.translation.*\") IS NOT NULL";
}, $mediaIds);
/** @var string $sql */
$sql = sprintf('
SELECT
*
FROM
`product_media`
WHERE
%s
', implode(' OR ', $conditions));
/** @var array $rows */
$rows = $this->connection->fetchAllAssociative($sql);
/** @var array $row */
foreach ($rows as $row) {
/** @var array $customFields */
$customFields = json_decode( $row['custom_fields'], true );
$customFields['p2labProductVideo']['translation'] = array_filter($customFields['p2labProductVideo']['translation'], function($mediaId) use($mediaIds){
return ! in_array($mediaId, $mediaIds);
});
$this->connection->update('product_media', [
'custom_fields' => json_encode($customFields),
], [
'id' => $row['id'],
]);
}
}
/**
* @param EntityDeletedEvent $event
* @return bool
*/
public function onMediaDeleted(EntityDeletedEvent $event): bool
{
/** @var array $mediaIds */
$mediaIds = $event->getIds();
$this->deleteRelatedMedia($mediaIds);
$this->deleteRelatedTranslations($mediaIds);
return true;
}
}