custom/plugins/CrayssnLabsRichSnippetsCreator/src/Subscriber/PageSubscriber.php line 139

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace CrayssnLabsRichSnippetsCreator\Subscriber;
  3. use CrayssnLabsRichSnippetsCreator\CrayssnLabsRichSnippetsCreator;
  4. use League\Flysystem\FilesystemInterface;
  5. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter;
  9. use Shopware\Core\System\SystemConfig\SystemConfigService;
  10. use Symfony\Component\Cache\Adapter\AdapterInterface;
  11. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  12. use Shopware\Core\Content\Cms\CmsPageEvents;
  13. use CrayssnLabsRichSnippetsCreator\Framework\Service\Google;
  14. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  15. /**
  16.  * Class PageSubscriber
  17.  *
  18.  * @package   CrayssnLabsRichSnippetsCreator\Subscriber
  19.  *
  20.  * @author    Sebastian Ludwig <dev@cl.team>
  21.  * @copyright Copyright (c) 2022, CrayssnLabs Ludwig Wiegler GbR
  22.  */
  23. class PageSubscriber implements EventSubscriberInterface
  24. {
  25.     /**
  26.      * @var \Shopware\Core\System\SystemConfig\SystemConfigService
  27.      */
  28.     private $systemConfigService;
  29.     /**
  30.      * @var \League\Flysystem\FilesystemInterface
  31.      */
  32.     private $fileSystemPublic;
  33.     /**
  34.      * @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
  35.      */
  36.     private $salesChannelRepository;
  37.     /**
  38.      * @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
  39.      */
  40.     private $themeRepository;
  41.     /**
  42.      * @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
  43.      */
  44.     private $mediaRepository;
  45.     /**
  46.      * @var string
  47.      */
  48.     private $salesChannelUrl;
  49.     /**
  50.      * @var string
  51.      */
  52.     private $themeMediaUrl;
  53.     /**
  54.      * @var string
  55.      */
  56.     private $googleApiKey;
  57.     /**
  58.      * @var string
  59.      */
  60.     private $placesId;
  61.     /**
  62.      * @var string
  63.      */
  64.     private $productName;
  65.     /**
  66.      * @var string
  67.      */
  68.     private $productDescription;
  69.     /**
  70.      * @var float
  71.      */
  72.     private $productPrice;
  73.     /**
  74.      * @var \Symfony\Component\Cache\Adapter\AdapterInterface
  75.      */
  76.     private $cache;
  77.     /**
  78.      * @param \Shopware\Core\System\SystemConfig\SystemConfigService                  $_systemConfigService
  79.      * @param \League\Flysystem\FilesystemInterface                                   $_fileSystemPublic
  80.      * @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_salesChannelRepository
  81.      * @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_themeRepository
  82.      * @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_mediaRepository
  83.      * @param \Symfony\Component\Cache\Adapter\AbstractAdapter                        $_cache
  84.      */
  85.     public function __construct(
  86.         SystemConfigService $_systemConfigService,
  87.         FilesystemInterface $_fileSystemPublic,
  88.         EntityRepositoryInterface $_salesChannelRepository,
  89.         EntityRepositoryInterface $_themeRepository,
  90.         EntityRepositoryInterface $_mediaRepository,
  91.         AdapterInterface $_cache
  92.     )
  93.     {
  94.         $this->systemConfigService $_systemConfigService;
  95.         $this->fileSystemPublic $_fileSystemPublic;
  96.         $this->salesChannelRepository $_salesChannelRepository;
  97.         $this->themeRepository $_themeRepository;
  98.         $this->mediaRepository $_mediaRepository;
  99.         $this->cache $_cache;
  100.     }
  101.     /**
  102.      * Function getSubscribedEvents
  103.      *
  104.      * @return string[]
  105.      */
  106.     public static function getSubscribedEvents(): array
  107.     {
  108.         return [
  109.             CmsPageEvents::PAGE_LOADED_EVENT => 'onPageLoaded'
  110.         ];
  111.     }
  112.     /**
  113.      * Function onPageLoaded
  114.      *
  115.      * @param \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent $event
  116.      *
  117.      * @throws \League\Flysystem\FileExistsException
  118.      * @throws \League\Flysystem\FileNotFoundException
  119.      * @throws \Psr\Cache\InvalidArgumentException
  120.      */
  121.     public function onPageLoaded(EntityLoadedEvent $event)
  122.     {
  123.         /**
  124.          * @var \Shopware\Core\Framework\Api\Context\SalesChannelApiSource $source
  125.          */
  126.         $source $event->getContext()->getSource();
  127.         // prevent execution for admin api calls
  128.         if(get_class($source) !== SalesChannelApiSource::class)
  129.         {
  130.             return;
  131.         }
  132.         $this->googleApiKey $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.googleApiKey'$source->getSalesChannelId());
  133.         $this->placesId $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.placesId'$source->getSalesChannelId());
  134.         if(empty($this->googleApiKey) || empty($this->placesId))
  135.         {
  136.             return;
  137.         }
  138.         $this->productName $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.productName'$source->getSalesChannelId());
  139.         $this->productDescription $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.productDescription'$source->getSalesChannelId());
  140.         $this->productPrice $this->systemConfigService->getFloat('CrayssnLabsRichSnippetsCreator.config.productPrice');
  141.         $this->prepare($event);
  142.         /**
  143.          * @var \Shopware\Core\Content\Cms\CmsPageEntity $entity
  144.          */
  145.         foreach ($event->getEntities() as $entity)
  146.         {
  147.             $struct = new Struct\RichSnippetsStruct();
  148.             $struct->setLdJson($this->getCachedLdJson());
  149.             $entity->addExtension(CrayssnLabsRichSnippetsCreator::PAGE_EXTENSION_NAME$struct);
  150.         }
  151.     }
  152.     /**
  153.      * Function getCachedLdJson
  154.      *
  155.      * @return string
  156.      * @throws \League\Flysystem\FileExistsException
  157.      * @throws \League\Flysystem\FileNotFoundException
  158.      * @throws \Psr\Cache\InvalidArgumentException
  159.      */
  160.     private function getCachedLdJson(): string
  161.     {
  162.         $item $this->cache->getItem(CrayssnLabsRichSnippetsCreator::CACHE_KEY);
  163.         if ($item->isHit())
  164.         {
  165.             return $item->get();
  166.         }
  167.         $ldJson $this->buildLdJson();
  168.         $item->set($ldJson);
  169.         //expires after one day
  170.         $item->expiresAfter(86400);
  171.         $this->cache->save($item);
  172.         return $ldJson;
  173.     }
  174.     /**
  175.      * Function buildLdJson
  176.      *
  177.      * @return string
  178.      * @throws \League\Flysystem\FileExistsException
  179.      * @throws \League\Flysystem\FileNotFoundException
  180.      */
  181.     private function buildLdJson(): string
  182.     {
  183.         return json_encode($this->buildStructureData());
  184.     }
  185.     /**
  186.      * Function getRichSnippedData
  187.      *
  188.      * @return array
  189.      */
  190.     private function getRichSnippedData(): array
  191.     {
  192.         $placeDetails Google\Places::requestDetailsByPlacesId($this->placesId$this->googleApiKey);
  193.         if(!empty($placeDetails))
  194.         {
  195.             return $placeDetails['result'];
  196.         }
  197.         return [];
  198.     }
  199.     /**
  200.      * Function buildStructureData
  201.      *
  202.      * @return array|null
  203.      * @throws \League\Flysystem\FileExistsException
  204.      * @throws \League\Flysystem\FileNotFoundException
  205.      */
  206.     private function buildStructureData(): ?array
  207.     {
  208.         $details $this->getRichSnippedData();
  209.         if(!empty($details))
  210.         {
  211.             $details['website'] = $this->salesChannelUrl;
  212.             return [
  213.                 '@context' => 'https://schema.org',
  214.                 '@type' => 'Product',
  215.                 'name' => empty($this->productName) ? $details['name'] : $this->productName,
  216.                 'description' => empty($this->productDescription) ? $details['formatted_address'] : $this->productDescription,
  217.                 'brand' => $this->prepareBrand($details),
  218.                 'image' => $this->prepareImage($details),
  219.                 'aggregateRating' => $this->prepareAggregateRating($details),
  220.                 'review' => $this->prepareReviews($details),
  221.                 'url' => $details['website'],
  222.                 'offers' => $this->prepareOffers($details),
  223.             ];
  224.         }
  225.         return null;
  226.     }
  227.     /**
  228.      * Function prepareAggregateRating
  229.      *
  230.      * @param array $_details
  231.      *
  232.      * @return array
  233.      */
  234.     private function prepareAggregateRating(array $_details): ?array
  235.     {
  236.         if(empty($_details['rating']))
  237.         {
  238.             return null;
  239.         }
  240.         return [
  241.             '@type' => 'AggregateRating',
  242.             'ratingValue' => $_details['rating'],
  243.             'bestRating' => 5,
  244.             'worstRating' => 1,
  245.             'ratingCount' => $_details['user_ratings_total'],
  246.         ];
  247.     }
  248.     private function prepareReviews(array $_details): ?array
  249.     {
  250.         $reviews = [];
  251.         if(empty($_details['reviews']))
  252.         {
  253.             return null;
  254.         }
  255.         foreach ($_details['reviews'] as $review)
  256.         {
  257.             $reviews[] = [
  258.                 '@type' => 'Review',
  259.                 'description' => $review['text'],
  260.                 'reviewRating' => [
  261.                     '@type' => 'Rating',
  262.                     'ratingValue' => $review['rating'],
  263.                     'bestRating' => 5,
  264.                     'worstRating' => 1,
  265.                 ],
  266.                 'author' => [
  267.                     '@type' => 'Person',
  268.                     'name' => $review['author_name']
  269.                 ]
  270.             ];
  271.         }
  272.         return $reviews;
  273.     }
  274.     /**
  275.      * Function prepareOffers
  276.      *
  277.      * @param array $_details
  278.      *
  279.      * @return array|null
  280.      */
  281.     private function prepareOffers(array $_details): ?array
  282.     {
  283.         if(empty($this->productPrice))
  284.         {
  285.             return null;
  286.         }
  287.         return [
  288.             '@type' => 'Offer',
  289.             'url' => $_details['website'],
  290.             'price' => $this->productPrice,
  291.             'priceCurrency' => 'EUR',
  292.             'availability' => 'https://schema.org/InStock',
  293.             'priceValidUntil' => date('Y-m-d'),
  294.         ];
  295.     }
  296.     /**
  297.      * Function prepareOffers
  298.      *
  299.      * @param array $_details
  300.      *
  301.      * @return array|null
  302.      */
  303.     private function prepareBrand(array $_details): ?array
  304.     {
  305.         return [
  306.             '@type' => 'Brand',
  307.             'url' => $_details['website'],
  308.             'name' => $_details['name'],
  309.         ];
  310.     }
  311.     /**
  312.      * Function prepareImage
  313.      *
  314.      * @param array $_details
  315.      *
  316.      * @return string|null
  317.      * @throws \League\Flysystem\FileExistsException
  318.      * @throws \League\Flysystem\FileNotFoundException
  319.      */
  320.     private function prepareImage(array $_details): ?string
  321.     {
  322.         $photoCacheFile CrayssnLabsRichSnippetsCreator::PUBLIC_CACHE_FOLDER DIRECTORY_SEPARATOR 'request-cache.jpg';
  323.         if(empty($_details['photos']))
  324.         {
  325.             return $this->themeMediaUrl;
  326.         }
  327.         /**
  328.          * @var array $photo = array ( 'height' => 2419, 'html_attributions' => array ( 0 => 'CrayssnLabs | Webdesign Erfurt', ), 'photo_reference' => 'AcYSjRgJa7ZM9TS6-4VXwwRSL9VI3_-NSxt2tkvBM23EmSRS173RGjsFXJYOy2VOT1rqeiVen-Uj31S08uv03RJdVUZunw0eOSRlcdcFy4VYfV71yD2w07LotaJAUJwPb53RLPiPD16ZnXsLcyMqnVXmnMn42OzrJd3wIFIs9XHNdgG7uvCL', 'width' => 4159, )
  329.          */
  330.         $photo $_details['photos'][0];
  331.         $photoUrl 'https://maps.googleapis.com/maps/api/place/photo?maxwidth=1920&photo_reference=' $photo['photo_reference'] . '&key=' $this->googleApiKey;
  332.         if($this->fileSystemPublic->has($photoCacheFile))
  333.         {
  334.             $this->fileSystemPublic->delete($photoCacheFile);
  335.         }
  336.         $this->fileSystemPublic->write($photoCacheFilefile_get_contents($photoUrl));
  337.         return rtrim($this->salesChannelUrl'/') . CrayssnLabsRichSnippetsCreator::PUBLIC_CACHE_FOLDER '/request-cache.jpg';
  338.     }
  339.     /**
  340.      * Function prepareUrl
  341.      *
  342.      * @param \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent $event
  343.      */
  344.     private function prepare(EntityLoadedEvent $event)
  345.     {
  346.         /**
  347.          * @var \Shopware\Core\Framework\Api\Context\SalesChannelApiSource $source
  348.          */
  349.         $source $event->getContext()->getSource();
  350.         $criteria = new Criteria();
  351.         $criteria->addAssociation('domains');
  352.         $criteria->addFilter(new Filter\EqualsFilter('id'$source->getSalesChannelId()));
  353.         $criteria->setLimit(1);
  354.         $salesChannels $this->salesChannelRepository->search($criteria$event->getContext());
  355.         /**
  356.          * @var \Shopware\Core\System\SalesChannel\SalesChannelEntity $salesChannel
  357.          */
  358.         $salesChannel $salesChannels->first();
  359.         $this->salesChannelUrl $salesChannel->getDomains()->first()->getUrl();
  360.         $criteria = new Criteria();
  361.         $criteria->addAssociation('salesChannels');
  362.         $criteria->addFilter(new Filter\EqualsFilter('salesChannels.id'$salesChannel->getId()));
  363.         $criteria->setLimit(1);
  364.         $themeSalesChannels $this->themeRepository->search($criteria$event->getContext());
  365.         /**
  366.          * @var \Shopware\Storefront\Theme\ThemeEntity $theme
  367.          */
  368.         $theme $themeSalesChannels->first();
  369.         $configValues $theme->getConfigValues();
  370.         if(empty($configValues['sw-logo-desktop']))
  371.         {
  372.             $baseConfig $theme->getBaseConfig();
  373.             $configValues['sw-logo-desktop'] = ['value' => $baseConfig['fields']['sw-logo-desktop']['value']];
  374.         }
  375.         $mediaId $configValues['sw-logo-desktop']['value'];
  376.         $medias $this->mediaRepository->search(new Criteria([$mediaId]), $event->getContext());
  377.         $media $medias->first();
  378.         if(empty($media))
  379.         {
  380.             return;
  381.         }
  382.         $this->themeMediaUrl $media->getUrl();
  383.     }
  384. }