<?php declare(strict_types=1);
namespace Acris\Faq\Components\Faq;
use Acris\Faq\Components\Faq\Struct\FaqVideoPreviewImageStruct;
use Acris\Faq\Components\Faq\Exception\FaqNotFoundException;
use Acris\Faq\Custom\FaqCollection;
use Acris\Faq\Custom\FaqEntity;
use Acris\Faq\Custom\FaqGroupCollection;
use Acris\Faq\Custom\FaqGroupEntity;
use Shopware\Core\Content\Media\MediaEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\HttpFoundation\Request;
class FaqService
{
public const DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE = 'youtube';
public const DEFAULT_FAQ_LAYOUT_TYPE = 'default';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK = 'https://www.youtube.com/watch?v=';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_OEMBED_URL = 'https://www.youtube.com/oembed?url=';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK_FORMAT = '&format=json';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR = '/watch?v=';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR = '/embed/';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_LINK = 'https://www.youtube.com/embed/';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_COOKIE_LINK = 'https://www.youtube-nocookie.com/embed/';
public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_PLAYER_CONTROLS = '?controls=0';
public const DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_IN_DAYS = 30;
public const DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_DAY_IN_SECONDS = 86400;
public const DEFAULT_CRITERIA_LIMIT = 50;
public const DEFAULT_VALUE_OFFSET = 0;
private EntityRepositoryInterface $faqRepository;
private EntityRepositoryInterface $faqGroupRepository;
private EntityRepositoryInterface $productStreamMappingRepository;
private EntityRepositoryInterface $mediaRepository;
private SystemConfigService $systemConfigService;
public function __construct(
EntityRepositoryInterface $faqRepository,
EntityRepositoryInterface $faqGroupRepository,
EntityRepositoryInterface $productStreamMappingRepository,
EntityRepositoryInterface $mediaRepository,
SystemConfigService $systemConfigService
)
{
$this->faqRepository = $faqRepository;
$this->faqGroupRepository = $faqGroupRepository;
$this->productStreamMappingRepository = $productStreamMappingRepository;
$this->mediaRepository = $mediaRepository;
$this->systemConfigService = $systemConfigService;
}
public function getFaqs(array $faqIds, Context $context): EntitySearchResult
{
$criteria = new Criteria([$faqIds]);
$criteria->addAssociation('media')
->addAssociation('groups')
->addAssociation('acrisFaqDocuments.languages')
->addAssociation('cmsPage');
return $this->faqRepository->search($criteria, $context);
}
public function getFaqGroups(array $faqGroupIds, Context $context): EntitySearchResult
{
return $this->faqGroupRepository->search((new Criteria($faqGroupIds))->addAssociation('productStreams')->addAssociation('faqs'), $context);
}
public function getProductIdsFromProductStreams(array $productStreamIds, Context $context): array
{
$productIds = [];
$productStreamMappingResultIds = $this->productStreamMappingRepository->searchIds((new Criteria())->addFilter(new EqualsAnyFilter('productStreamId', $productStreamIds)), $context);
foreach ($productStreamMappingResultIds->getIds() as $productStreamMappingId) {
if (!empty($productStreamMappingId) && is_array($productStreamMappingId) && array_key_exists('productId', $productStreamMappingId) && !empty($productStreamMappingId['productId'])) {
$productIds[] = $productStreamMappingId['productId'];
}
}
return $productIds;
}
public function getFaqById(string $faqId, SalesChannelContext $salesChannelContext, Request $request): FaqEntity
{
$criteria = new Criteria([$faqId]);
$criteria->addAssociation('media')
->addAssociation('groups')
->addAssociation('acrisFaqDocuments.languages')
->addAssociation('cmsPage')
->addFilter(new EqualsFilter('active', true))
->addFilter(new EqualsFilter('groups.active', true));
$faq = $this->faqRepository->search($criteria, $salesChannelContext->getContext())->first();
if (empty($faq) || !$faq instanceof FaqEntity) {
throw new FaqNotFoundException($faqId);
}
if (!empty($faq->getTranslation('embedCode')) && $faq->getVideoType() === self::DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE) {
$this->loadFaqVideo($faq, $salesChannelContext);
}
$this->checkLanguage([], $salesChannelContext->getContext()->getLanguageId(), new FaqCollection([$faq]));
return $faq;
}
public function loadFaqVideo(FaqEntity $faq, SalesChannelContext $context): void
{
$videoId = $this->getVideoId($faq);
$this->assignVideoUrl($faq, $videoId, $context);
}
public function checkLanguage(array $faqGroups, string $languageId, ?FaqCollection $faqCollection = null): array
{
if (!empty($faqCollection) && $faqCollection->count() > 0) {
$this->checkLanguageForFaq($faqCollection, $languageId);
return $faqGroups;
}
/** @var FaqGroupEntity $faqGroup */
foreach ($faqGroups as $faqGroup) {
if (empty($faqGroup->getFaqs()) || $faqGroup->getFaqs()->count() === 0) continue;
$this->checkLanguageForFaq($faqGroup->getFaqs(), $languageId);
}
return $faqGroups;
}
public function assignPreviewImage(FaqGroupCollection $faqGroups, SalesChannelContext $context): void
{
if ($faqGroups->count() === 0) return;
$previewImageId = $this->systemConfigService->get('AcrisFaqCS.config.previewImage', $context->getSalesChannel()->getId());
if (empty($previewImageId)) return;
/** @var MediaEntity $previewImage */
$previewImage = $this->mediaRepository->search((new Criteria([$previewImageId])), $context->getContext())->first();
if (empty($previewImage)) return;
$previewImageStruct = new FaqVideoPreviewImageStruct($previewImage);
foreach ($faqGroups as $faqGroup) {
if (empty($faqGroup->getFaqs()) || $faqGroup->getFaqs()->count() === 0) continue;
foreach ($faqGroup->getFaqs() as $faq) {
if (!empty($faq->getTranslation('embedCode'))) {
$faq->addExtension('acrisFaqVideoPreviewImage', $previewImageStruct);
}
}
}
}
public function executeTask(): void
{
$context = Context::createDefaultContext();
$criteria = $this->loadCriteria(new Criteria());
$this->upsertYoutubeMetaDataWithFaqIds($criteria, $context);
}
public function getValidMetaDataUntil(): int
{
$validMetaDataUntil = $this->systemConfigService->get('AcrisFaqCS.config.metaData');
return !empty($validMetaDataUntil) ? intval($validMetaDataUntil) : self::DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_IN_DAYS;
}
public function upsertYoutubeMetaDataWithFaqIds(Criteria $criteria, Context $context): void
{
$updateData = [];
$offset = self::DEFAULT_VALUE_OFFSET;
$load = true;
while ($load) {
$criteria->setOffset($offset);
/** @var EntitySearchResult $faqSearchResult */
$faqSearchResult = $this->faqRepository->search($criteria, $context);
if ($faqSearchResult->count() > 0) {
/** @var FaqEntity $faq */
foreach ($faqSearchResult->getEntities()->getElements() as $faq) {
$videoId = $this->getVideoId($faq);
$metaData = $this->createYoutubeWatchUrl($videoId);
if (!empty($metaData) && is_array($metaData)) {
$updateData[] = [
'id' => $faq->getId(),
'metaData' => $metaData,
'metaDataCreatedAt' => new \DateTimeImmutable()
];
}
}
}
$offset += self::DEFAULT_CRITERIA_LIMIT;
if ($faqSearchResult->getTotal() < self::DEFAULT_CRITERIA_LIMIT) $load = false;
}
if (!empty($updateData)) {
$this->faqRepository->update($updateData, $context);
}
}
public function loadCriteria(Criteria $criteria): Criteria
{
$validMetaDataUntil = $this->getValidMetaDataUntil();
$compareTime = (new \DateTime('-' . strval($validMetaDataUntil) . ' days'))->format('Y-m-d H:i:s');
return $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
new MultiFilter(MultiFilter::CONNECTION_OR, [
new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('metaData', null),
new EqualsFilter('metaDataCreatedAt', null)
]),
new RangeFilter('metaDataCreatedAt', ['lte' => $compareTime])
]),
new NotFilter(NotFilter::CONNECTION_AND, [
new EqualsFilter('embedCode', null)
]),
new EqualsFilter('videoType', self::DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE)
]));
}
private function getVideoId(FaqEntity $faq): string
{
$videoUrl = $faq->getTranslation('embedCode');
if (str_contains($videoUrl, self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR)) {
$urlParameters = explode(self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR, $videoUrl);
return !empty($urlParameters) && is_array($urlParameters) && array_key_exists(1, $urlParameters) && !empty($urlParameters[1]) ? $urlParameters[1] : $videoUrl;
} elseif (str_contains($videoUrl, self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR)) {
$urlParameters = explode(self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR, $videoUrl);
return !empty($urlParameters) && is_array($urlParameters) && array_key_exists(1, $urlParameters) && !empty($urlParameters[1]) ? $urlParameters[1] : $videoUrl;
}
return $videoUrl;
}
private function assignVideoUrl(FaqEntity $faq, string $videoId, SalesChannelContext $context): void
{
$validMetaDataUntil = $this->systemConfigService->get('AcrisFaqCS.config.metaData', $context->getSalesChannel()->getId());
$validMetaDataUntil = !empty($validMetaDataUntil) ? $validMetaDataUntil * self::DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_DAY_IN_SECONDS : self::DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_IN_DAYS * self::DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_DAY_IN_SECONDS;
$privacyMode = $faq->getVideoPrivacyMode() === true ? self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_COOKIE_LINK : self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_LINK;
$playerControls = $faq->getVideoPlayerControls() === true ? '' : self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_PLAYER_CONTROLS;
$faq->addTranslated('embedCode', $privacyMode . $videoId . $playerControls);
if (empty($faq->getMetaData()) || empty($faq->getMetaDataCreatedAt()) || ($faq->getMetaDataCreatedAt()->getTimestamp() < ((new \DateTime())->getTimestamp() - $validMetaDataUntil))) {
$this->upsertYoutubeMetaData($videoId, $faq, $context->getContext());
}
}
private function createYoutubeWatchUrl(string $videoId): ?array
{
try {
$url = self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK . $videoId;
$urlData = $this->getYoutubeMetaData($url);
$metaDescription = get_meta_tags($url);
if (!empty($urlData) && !empty($metaDescription) && array_key_exists('description', $metaDescription)) {
$urlData['description'] = $metaDescription['description'];
}
return $urlData;
} catch (\Throwable $e) {
return null;
}
}
private function getYoutubeMetaData($url): ?array
{
$youtube = self::DEFAULT_FAQ_VIDEO_YOUTUBE_OEMBED_URL . $url . self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK_FORMAT;
$curl = curl_init($youtube);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$return = curl_exec($curl);
curl_close($curl);
return !empty($return) && is_string($return) ? json_decode($return, true) : null;
}
private function checkLanguageForFaq(FaqCollection $faqCollection, string $languageId): void
{
foreach ($faqCollection->getElements() as $faq) {
if (empty($faq->getAcrisFaqDocuments()) || $faq->getAcrisFaqDocuments()->count() === 0) continue;
foreach ($faq->getAcrisFaqDocuments()->getElements() as $document) {
if (empty($document->getLanguages()) || $document->getLanguages()->count() === 0) continue;
foreach ($document->getLanguages() as $languageEntity) {
if ($languageEntity->getId() === $languageId) continue 2;
}
$faq->getAcrisFaqDocuments()->remove($document->getId());
}
}
}
private function upsertYoutubeMetaData(string $videoId, FaqEntity $faq, Context $context): void
{
$metaData = $this->createYoutubeWatchUrl($videoId);
if (!empty($metaData) && is_array($metaData)) {
$this->faqRepository->upsert([
[
'id' => $faq->getId(),
'metaData' => $metaData,
'metaDataCreatedAt' => new \DateTimeImmutable()
]
], $context);
}
}
}