custom/plugins/AcrisFaqCS/src/Components/Faq/FaqService.php line 106

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Acris\Faq\Components\Faq;
  3. use Acris\Faq\Components\Faq\Struct\FaqVideoPreviewImageStruct;
  4. use Acris\Faq\Components\Faq\Exception\FaqNotFoundException;
  5. use Acris\Faq\Custom\FaqCollection;
  6. use Acris\Faq\Custom\FaqEntity;
  7. use Acris\Faq\Custom\FaqGroupCollection;
  8. use Acris\Faq\Custom\FaqGroupEntity;
  9. use Shopware\Core\Content\Media\MediaEntity;
  10. use Shopware\Core\Framework\Context;
  11. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  19. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  20. use Shopware\Core\System\SystemConfig\SystemConfigService;
  21. use Symfony\Component\HttpFoundation\Request;
  22. class FaqService
  23. {
  24. public const DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE = 'youtube';
  25. public const DEFAULT_FAQ_LAYOUT_TYPE = 'default';
  26. public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK = 'https://www.youtube.com/watch?v=';
  27. public const DEFAULT_FAQ_VIDEO_YOUTUBE_OEMBED_URL = 'https://www.youtube.com/oembed?url=';
  28. public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK_FORMAT = '&format=json';
  29. public const DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR = '/watch?v=';
  30. public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR = '/embed/';
  31. public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_LINK = 'https://www.youtube.com/embed/';
  32. public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_COOKIE_LINK = 'https://www.youtube-nocookie.com/embed/';
  33. public const DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_PLAYER_CONTROLS = '?controls=0';
  34. public const DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_IN_DAYS = 30;
  35. public const DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_DAY_IN_SECONDS = 86400;
  36. public const DEFAULT_CRITERIA_LIMIT = 50;
  37. public const DEFAULT_VALUE_OFFSET = 0;
  38. private EntityRepositoryInterface $faqRepository;
  39. private EntityRepositoryInterface $faqGroupRepository;
  40. private EntityRepositoryInterface $productStreamMappingRepository;
  41. private EntityRepositoryInterface $mediaRepository;
  42. private SystemConfigService $systemConfigService;
  43. public function __construct(
  44. EntityRepositoryInterface $faqRepository,
  45. EntityRepositoryInterface $faqGroupRepository,
  46. EntityRepositoryInterface $productStreamMappingRepository,
  47. EntityRepositoryInterface $mediaRepository,
  48. SystemConfigService $systemConfigService
  49. )
  50. {
  51. $this->faqRepository = $faqRepository;
  52. $this->faqGroupRepository = $faqGroupRepository;
  53. $this->productStreamMappingRepository = $productStreamMappingRepository;
  54. $this->mediaRepository = $mediaRepository;
  55. $this->systemConfigService = $systemConfigService;
  56. }
  57. public function getFaqs(array $faqIds, Context $context): EntitySearchResult
  58. {
  59. $criteria = new Criteria([$faqIds]);
  60. $criteria->addAssociation('media')
  61. ->addAssociation('groups')
  62. ->addAssociation('acrisFaqDocuments.languages')
  63. ->addAssociation('cmsPage');
  64. return $this->faqRepository->search($criteria, $context);
  65. }
  66. public function getFaqGroups(array $faqGroupIds, Context $context): EntitySearchResult
  67. {
  68. return $this->faqGroupRepository->search((new Criteria($faqGroupIds))->addAssociation('productStreams')->addAssociation('faqs'), $context);
  69. }
  70. public function getProductIdsFromProductStreams(array $productStreamIds, Context $context): array
  71. {
  72. $productIds = [];
  73. $productStreamMappingResultIds = $this->productStreamMappingRepository->searchIds((new Criteria())->addFilter(new EqualsAnyFilter('productStreamId', $productStreamIds)), $context);
  74. foreach ($productStreamMappingResultIds->getIds() as $productStreamMappingId) {
  75. if (!empty($productStreamMappingId) && is_array($productStreamMappingId) && array_key_exists('productId', $productStreamMappingId) && !empty($productStreamMappingId['productId'])) {
  76. $productIds[] = $productStreamMappingId['productId'];
  77. }
  78. }
  79. return $productIds;
  80. }
  81. public function getFaqById(string $faqId, SalesChannelContext $salesChannelContext, Request $request): FaqEntity
  82. {
  83. $criteria = new Criteria([$faqId]);
  84. $criteria->addAssociation('media')
  85. ->addAssociation('groups')
  86. ->addAssociation('acrisFaqDocuments.languages')
  87. ->addAssociation('cmsPage')
  88. ->addFilter(new EqualsFilter('active', true))
  89. ->addFilter(new EqualsFilter('groups.active', true));
  90. $faq = $this->faqRepository->search($criteria, $salesChannelContext->getContext())->first();
  91. if (empty($faq) || !$faq instanceof FaqEntity) {
  92. throw new FaqNotFoundException($faqId);
  93. }
  94. if (!empty($faq->getTranslation('embedCode')) && $faq->getVideoType() === self::DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE) {
  95. $this->loadFaqVideo($faq, $salesChannelContext);
  96. }
  97. $this->checkLanguage([], $salesChannelContext->getContext()->getLanguageId(), new FaqCollection([$faq]));
  98. return $faq;
  99. }
  100. public function loadFaqVideo(FaqEntity $faq, SalesChannelContext $context): void
  101. {
  102. $videoId = $this->getVideoId($faq);
  103. $this->assignVideoUrl($faq, $videoId, $context);
  104. }
  105. public function checkLanguage(array $faqGroups, string $languageId, ?FaqCollection $faqCollection = null): array
  106. {
  107. if (!empty($faqCollection) && $faqCollection->count() > 0) {
  108. $this->checkLanguageForFaq($faqCollection, $languageId);
  109. return $faqGroups;
  110. }
  111. /** @var FaqGroupEntity $faqGroup */
  112. foreach ($faqGroups as $faqGroup) {
  113. if (empty($faqGroup->getFaqs()) || $faqGroup->getFaqs()->count() === 0) continue;
  114. $this->checkLanguageForFaq($faqGroup->getFaqs(), $languageId);
  115. }
  116. return $faqGroups;
  117. }
  118. public function assignPreviewImage(FaqGroupCollection $faqGroups, SalesChannelContext $context): void
  119. {
  120. if ($faqGroups->count() === 0) return;
  121. $previewImageId = $this->systemConfigService->get('AcrisFaqCS.config.previewImage', $context->getSalesChannel()->getId());
  122. if (empty($previewImageId)) return;
  123. /** @var MediaEntity $previewImage */
  124. $previewImage = $this->mediaRepository->search((new Criteria([$previewImageId])), $context->getContext())->first();
  125. if (empty($previewImage)) return;
  126. $previewImageStruct = new FaqVideoPreviewImageStruct($previewImage);
  127. foreach ($faqGroups as $faqGroup) {
  128. if (empty($faqGroup->getFaqs()) || $faqGroup->getFaqs()->count() === 0) continue;
  129. foreach ($faqGroup->getFaqs() as $faq) {
  130. if (!empty($faq->getTranslation('embedCode'))) {
  131. $faq->addExtension('acrisFaqVideoPreviewImage', $previewImageStruct);
  132. }
  133. }
  134. }
  135. }
  136. public function executeTask(): void
  137. {
  138. $context = Context::createDefaultContext();
  139. $criteria = $this->loadCriteria(new Criteria());
  140. $this->upsertYoutubeMetaDataWithFaqIds($criteria, $context);
  141. }
  142. public function getValidMetaDataUntil(): int
  143. {
  144. $validMetaDataUntil = $this->systemConfigService->get('AcrisFaqCS.config.metaData');
  145. return !empty($validMetaDataUntil) ? intval($validMetaDataUntil) : self::DEFAULT_FAQ_VIDEO_META_DATA_VALIDATE_TIME_IN_DAYS;
  146. }
  147. public function upsertYoutubeMetaDataWithFaqIds(Criteria $criteria, Context $context): void
  148. {
  149. $updateData = [];
  150. $offset = self::DEFAULT_VALUE_OFFSET;
  151. $load = true;
  152. while ($load) {
  153. $criteria->setOffset($offset);
  154. /** @var EntitySearchResult $faqSearchResult */
  155. $faqSearchResult = $this->faqRepository->search($criteria, $context);
  156. if ($faqSearchResult->count() > 0) {
  157. /** @var FaqEntity $faq */
  158. foreach ($faqSearchResult->getEntities()->getElements() as $faq) {
  159. $videoId = $this->getVideoId($faq);
  160. $metaData = $this->createYoutubeWatchUrl($videoId);
  161. if (!empty($metaData) && is_array($metaData)) {
  162. $updateData[] = [
  163. 'id' => $faq->getId(),
  164. 'metaData' => $metaData,
  165. 'metaDataCreatedAt' => new \DateTimeImmutable()
  166. ];
  167. }
  168. }
  169. }
  170. $offset += self::DEFAULT_CRITERIA_LIMIT;
  171. if ($faqSearchResult->getTotal() < self::DEFAULT_CRITERIA_LIMIT) $load = false;
  172. }
  173. if (!empty($updateData)) {
  174. $this->faqRepository->update($updateData, $context);
  175. }
  176. }
  177. public function loadCriteria(Criteria $criteria): Criteria
  178. {
  179. $validMetaDataUntil = $this->getValidMetaDataUntil();
  180. $compareTime = (new \DateTime('-' . strval($validMetaDataUntil) . ' days'))->format('Y-m-d H:i:s');
  181. return $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  182. new MultiFilter(MultiFilter::CONNECTION_OR, [
  183. new MultiFilter(MultiFilter::CONNECTION_AND, [
  184. new EqualsFilter('metaData', null),
  185. new EqualsFilter('metaDataCreatedAt', null)
  186. ]),
  187. new RangeFilter('metaDataCreatedAt', ['lte' => $compareTime])
  188. ]),
  189. new NotFilter(NotFilter::CONNECTION_AND, [
  190. new EqualsFilter('embedCode', null)
  191. ]),
  192. new EqualsFilter('videoType', self::DEFAULT_FAQ_VIDEO_YOUTUBE_TYPE)
  193. ]));
  194. }
  195. private function getVideoId(FaqEntity $faq): string
  196. {
  197. $videoUrl = $faq->getTranslation('embedCode');
  198. if (str_contains($videoUrl, self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR)) {
  199. $urlParameters = explode(self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_SEPARATOR, $videoUrl);
  200. return !empty($urlParameters) && is_array($urlParameters) && array_key_exists(1, $urlParameters) && !empty($urlParameters[1]) ? $urlParameters[1] : $videoUrl;
  201. } elseif (str_contains($videoUrl, self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR)) {
  202. $urlParameters = explode(self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_SEPARATOR, $videoUrl);
  203. return !empty($urlParameters) && is_array($urlParameters) && array_key_exists(1, $urlParameters) && !empty($urlParameters[1]) ? $urlParameters[1] : $videoUrl;
  204. }
  205. return $videoUrl;
  206. }
  207. private function assignVideoUrl(FaqEntity $faq, string $videoId, SalesChannelContext $context): void
  208. {
  209. $validMetaDataUntil = $this->systemConfigService->get('AcrisFaqCS.config.metaData', $context->getSalesChannel()->getId());
  210. $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;
  211. $privacyMode = $faq->getVideoPrivacyMode() === true ? self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_COOKIE_LINK : self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_LINK;
  212. $playerControls = $faq->getVideoPlayerControls() === true ? '' : self::DEFAULT_FAQ_VIDEO_YOUTUBE_EMBED_NO_PLAYER_CONTROLS;
  213. $faq->addTranslated('embedCode', $privacyMode . $videoId . $playerControls);
  214. if (empty($faq->getMetaData()) || empty($faq->getMetaDataCreatedAt()) || ($faq->getMetaDataCreatedAt()->getTimestamp() < ((new \DateTime())->getTimestamp() - $validMetaDataUntil))) {
  215. $this->upsertYoutubeMetaData($videoId, $faq, $context->getContext());
  216. }
  217. }
  218. private function createYoutubeWatchUrl(string $videoId): ?array
  219. {
  220. try {
  221. $url = self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK . $videoId;
  222. $urlData = $this->getYoutubeMetaData($url);
  223. $metaDescription = get_meta_tags($url);
  224. if (!empty($urlData) && !empty($metaDescription) && array_key_exists('description', $metaDescription)) {
  225. $urlData['description'] = $metaDescription['description'];
  226. }
  227. return $urlData;
  228. } catch (\Throwable $e) {
  229. return null;
  230. }
  231. }
  232. private function getYoutubeMetaData($url): ?array
  233. {
  234. $youtube = self::DEFAULT_FAQ_VIDEO_YOUTUBE_OEMBED_URL . $url . self::DEFAULT_FAQ_VIDEO_YOUTUBE_WATCH_LINK_FORMAT;
  235. $curl = curl_init($youtube);
  236. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
  237. $return = curl_exec($curl);
  238. curl_close($curl);
  239. return !empty($return) && is_string($return) ? json_decode($return, true) : null;
  240. }
  241. private function checkLanguageForFaq(FaqCollection $faqCollection, string $languageId): void
  242. {
  243. foreach ($faqCollection->getElements() as $faq) {
  244. if (empty($faq->getAcrisFaqDocuments()) || $faq->getAcrisFaqDocuments()->count() === 0) continue;
  245. foreach ($faq->getAcrisFaqDocuments()->getElements() as $document) {
  246. if (empty($document->getLanguages()) || $document->getLanguages()->count() === 0) continue;
  247. foreach ($document->getLanguages() as $languageEntity) {
  248. if ($languageEntity->getId() === $languageId) continue 2;
  249. }
  250. $faq->getAcrisFaqDocuments()->remove($document->getId());
  251. }
  252. }
  253. }
  254. private function upsertYoutubeMetaData(string $videoId, FaqEntity $faq, Context $context): void
  255. {
  256. $metaData = $this->createYoutubeWatchUrl($videoId);
  257. if (!empty($metaData) && is_array($metaData)) {
  258. $this->faqRepository->upsert([
  259. [
  260. 'id' => $faq->getId(),
  261. 'metaData' => $metaData,
  262. 'metaDataCreatedAt' => new \DateTimeImmutable()
  263. ]
  264. ], $context);
  265. }
  266. }
  267. }