<?php declare(strict_types=1);
namespace CrayssnLabsRichSnippetsCreator\Subscriber;
use CrayssnLabsRichSnippetsCreator\CrayssnLabsRichSnippetsCreator;
use League\Flysystem\FilesystemInterface;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Content\Cms\CmsPageEvents;
use CrayssnLabsRichSnippetsCreator\Framework\Service\Google;
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
/**
* Class PageSubscriber
*
* @package CrayssnLabsRichSnippetsCreator\Subscriber
*
* @author Sebastian Ludwig <dev@cl.team>
* @copyright Copyright (c) 2022, CrayssnLabs Ludwig Wiegler GbR
*/
class PageSubscriber implements EventSubscriberInterface
{
/**
* @var \Shopware\Core\System\SystemConfig\SystemConfigService
*/
private $systemConfigService;
/**
* @var \League\Flysystem\FilesystemInterface
*/
private $fileSystemPublic;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $salesChannelRepository;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $themeRepository;
/**
* @var \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface
*/
private $mediaRepository;
/**
* @var string
*/
private $salesChannelUrl;
/**
* @var string
*/
private $themeMediaUrl;
/**
* @var string
*/
private $googleApiKey;
/**
* @var string
*/
private $placesId;
/**
* @var string
*/
private $productName;
/**
* @var string
*/
private $productDescription;
/**
* @var float
*/
private $productPrice;
/**
* @var \Symfony\Component\Cache\Adapter\AdapterInterface
*/
private $cache;
/**
* @param \Shopware\Core\System\SystemConfig\SystemConfigService $_systemConfigService
* @param \League\Flysystem\FilesystemInterface $_fileSystemPublic
* @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_salesChannelRepository
* @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_themeRepository
* @param \Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface $_mediaRepository
* @param \Symfony\Component\Cache\Adapter\AbstractAdapter $_cache
*/
public function __construct(
SystemConfigService $_systemConfigService,
FilesystemInterface $_fileSystemPublic,
EntityRepositoryInterface $_salesChannelRepository,
EntityRepositoryInterface $_themeRepository,
EntityRepositoryInterface $_mediaRepository,
AdapterInterface $_cache
)
{
$this->systemConfigService = $_systemConfigService;
$this->fileSystemPublic = $_fileSystemPublic;
$this->salesChannelRepository = $_salesChannelRepository;
$this->themeRepository = $_themeRepository;
$this->mediaRepository = $_mediaRepository;
$this->cache = $_cache;
}
/**
* Function getSubscribedEvents
*
* @return string[]
*/
public static function getSubscribedEvents(): array
{
return [
CmsPageEvents::PAGE_LOADED_EVENT => 'onPageLoaded'
];
}
/**
* Function onPageLoaded
*
* @param \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent $event
*
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
* @throws \Psr\Cache\InvalidArgumentException
*/
public function onPageLoaded(EntityLoadedEvent $event)
{
/**
* @var \Shopware\Core\Framework\Api\Context\SalesChannelApiSource $source
*/
$source = $event->getContext()->getSource();
// prevent execution for admin api calls
if(get_class($source) !== SalesChannelApiSource::class)
{
return;
}
$this->googleApiKey = $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.googleApiKey', $source->getSalesChannelId());
$this->placesId = $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.placesId', $source->getSalesChannelId());
if(empty($this->googleApiKey) || empty($this->placesId))
{
return;
}
$this->productName = $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.productName', $source->getSalesChannelId());
$this->productDescription = $this->systemConfigService->getString('CrayssnLabsRichSnippetsCreator.config.productDescription', $source->getSalesChannelId());
$this->productPrice = $this->systemConfigService->getFloat('CrayssnLabsRichSnippetsCreator.config.productPrice');
$this->prepare($event);
/**
* @var \Shopware\Core\Content\Cms\CmsPageEntity $entity
*/
foreach ($event->getEntities() as $entity)
{
$struct = new Struct\RichSnippetsStruct();
$struct->setLdJson($this->getCachedLdJson());
$entity->addExtension(CrayssnLabsRichSnippetsCreator::PAGE_EXTENSION_NAME, $struct);
}
}
/**
* Function getCachedLdJson
*
* @return string
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
* @throws \Psr\Cache\InvalidArgumentException
*/
private function getCachedLdJson(): string
{
$item = $this->cache->getItem(CrayssnLabsRichSnippetsCreator::CACHE_KEY);
if ($item->isHit())
{
return $item->get();
}
$ldJson = $this->buildLdJson();
$item->set($ldJson);
//expires after one day
$item->expiresAfter(86400);
$this->cache->save($item);
return $ldJson;
}
/**
* Function buildLdJson
*
* @return string
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
*/
private function buildLdJson(): string
{
return json_encode($this->buildStructureData());
}
/**
* Function getRichSnippedData
*
* @return array
*/
private function getRichSnippedData(): array
{
$placeDetails = Google\Places::requestDetailsByPlacesId($this->placesId, $this->googleApiKey);
if(!empty($placeDetails))
{
return $placeDetails['result'];
}
return [];
}
/**
* Function buildStructureData
*
* @return array|null
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
*/
private function buildStructureData(): ?array
{
$details = $this->getRichSnippedData();
if(!empty($details))
{
$details['website'] = $this->salesChannelUrl;
return [
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => empty($this->productName) ? $details['name'] : $this->productName,
'description' => empty($this->productDescription) ? $details['formatted_address'] : $this->productDescription,
'brand' => $this->prepareBrand($details),
'image' => $this->prepareImage($details),
'aggregateRating' => $this->prepareAggregateRating($details),
'review' => $this->prepareReviews($details),
'url' => $details['website'],
'offers' => $this->prepareOffers($details),
];
}
return null;
}
/**
* Function prepareAggregateRating
*
* @param array $_details
*
* @return array
*/
private function prepareAggregateRating(array $_details): ?array
{
if(empty($_details['rating']))
{
return null;
}
return [
'@type' => 'AggregateRating',
'ratingValue' => $_details['rating'],
'bestRating' => 5,
'worstRating' => 1,
'ratingCount' => $_details['user_ratings_total'],
];
}
private function prepareReviews(array $_details): ?array
{
$reviews = [];
if(empty($_details['reviews']))
{
return null;
}
foreach ($_details['reviews'] as $review)
{
$reviews[] = [
'@type' => 'Review',
'description' => $review['text'],
'reviewRating' => [
'@type' => 'Rating',
'ratingValue' => $review['rating'],
'bestRating' => 5,
'worstRating' => 1,
],
'author' => [
'@type' => 'Person',
'name' => $review['author_name']
]
];
}
return $reviews;
}
/**
* Function prepareOffers
*
* @param array $_details
*
* @return array|null
*/
private function prepareOffers(array $_details): ?array
{
if(empty($this->productPrice))
{
return null;
}
return [
'@type' => 'Offer',
'url' => $_details['website'],
'price' => $this->productPrice,
'priceCurrency' => 'EUR',
'availability' => 'https://schema.org/InStock',
'priceValidUntil' => date('Y-m-d'),
];
}
/**
* Function prepareOffers
*
* @param array $_details
*
* @return array|null
*/
private function prepareBrand(array $_details): ?array
{
return [
'@type' => 'Brand',
'url' => $_details['website'],
'name' => $_details['name'],
];
}
/**
* Function prepareImage
*
* @param array $_details
*
* @return string|null
* @throws \League\Flysystem\FileExistsException
* @throws \League\Flysystem\FileNotFoundException
*/
private function prepareImage(array $_details): ?string
{
$photoCacheFile = CrayssnLabsRichSnippetsCreator::PUBLIC_CACHE_FOLDER . DIRECTORY_SEPARATOR . 'request-cache.jpg';
if(empty($_details['photos']))
{
return $this->themeMediaUrl;
}
/**
* @var array $photo = array ( 'height' => 2419, 'html_attributions' => array ( 0 => 'CrayssnLabs | Webdesign Erfurt', ), 'photo_reference' => 'AcYSjRgJa7ZM9TS6-4VXwwRSL9VI3_-NSxt2tkvBM23EmSRS173RGjsFXJYOy2VOT1rqeiVen-Uj31S08uv03RJdVUZunw0eOSRlcdcFy4VYfV71yD2w07LotaJAUJwPb53RLPiPD16ZnXsLcyMqnVXmnMn42OzrJd3wIFIs9XHNdgG7uvCL', 'width' => 4159, )
*/
$photo = $_details['photos'][0];
$photoUrl = 'https://maps.googleapis.com/maps/api/place/photo?maxwidth=1920&photo_reference=' . $photo['photo_reference'] . '&key=' . $this->googleApiKey;
if($this->fileSystemPublic->has($photoCacheFile))
{
$this->fileSystemPublic->delete($photoCacheFile);
}
$this->fileSystemPublic->write($photoCacheFile, file_get_contents($photoUrl));
return rtrim($this->salesChannelUrl, '/') . CrayssnLabsRichSnippetsCreator::PUBLIC_CACHE_FOLDER . '/request-cache.jpg';
}
/**
* Function prepareUrl
*
* @param \Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent $event
*/
private function prepare(EntityLoadedEvent $event)
{
/**
* @var \Shopware\Core\Framework\Api\Context\SalesChannelApiSource $source
*/
$source = $event->getContext()->getSource();
$criteria = new Criteria();
$criteria->addAssociation('domains');
$criteria->addFilter(new Filter\EqualsFilter('id', $source->getSalesChannelId()));
$criteria->setLimit(1);
$salesChannels = $this->salesChannelRepository->search($criteria, $event->getContext());
/**
* @var \Shopware\Core\System\SalesChannel\SalesChannelEntity $salesChannel
*/
$salesChannel = $salesChannels->first();
$this->salesChannelUrl = $salesChannel->getDomains()->first()->getUrl();
$criteria = new Criteria();
$criteria->addAssociation('salesChannels');
$criteria->addFilter(new Filter\EqualsFilter('salesChannels.id', $salesChannel->getId()));
$criteria->setLimit(1);
$themeSalesChannels = $this->themeRepository->search($criteria, $event->getContext());
/**
* @var \Shopware\Storefront\Theme\ThemeEntity $theme
*/
$theme = $themeSalesChannels->first();
$configValues = $theme->getConfigValues();
if(empty($configValues['sw-logo-desktop']))
{
$baseConfig = $theme->getBaseConfig();
$configValues['sw-logo-desktop'] = ['value' => $baseConfig['fields']['sw-logo-desktop']['value']];
}
$mediaId = $configValues['sw-logo-desktop']['value'];
$medias = $this->mediaRepository->search(new Criteria([$mediaId]), $event->getContext());
$media = $medias->first();
if(empty($media))
{
return;
}
$this->themeMediaUrl = $media->getUrl();
}
}