<?php declare(strict_types=1);
namespace DonCarneTheme\Subscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
use DonCarneTheme\Service\GraphQLClient;
class ProductSubscriber implements EventSubscriberInterface
{
private SystemConfigService $systemConfigService;
private EntityRepository $propertyGroupOptionsRepository;
private EntityRepository $productRepository;
private GraphQLClient $graphQLClient;
public function __construct(
SystemConfigService $systemConfigService,
EntityRepository $propertyGroupOptionsRepository,
EntityRepository $productRepository,
GraphQLClient $graphQLClient
) {
$this->systemConfigService = $systemConfigService;
$this->propertyGroupOptionsRepository = $propertyGroupOptionsRepository;
$this->productRepository = $productRepository;
$this->graphQLClient = $graphQLClient;
}
public static function getSubscribedEvents(): array
{
return [
'sales_channel.' . ProductEvents::PRODUCT_LOADED_EVENT => 'onProductLoaded',
ProductPageLoadedEvent::class => 'onProductDetailPageLoaded'
];
}
public function onProductLoaded(SalesChannelEntityLoadedEvent $event)
{
$products = $event->getEntities();
if (empty($products)) {
return;
}
$isMultipleProducts = count($products) > 1 ? true : false;
$themeConfig = $this->systemConfigService->get("DonCarneTheme.config");
$this->loadSpecialProperties($event, $themeConfig, $isMultipleProducts, $products);
$this->loadCutImages($event, $themeConfig, $isMultipleProducts, $products);
$this->loadBadgeProperties($event, $themeConfig, $isMultipleProducts, $products);
}
public function onProductDetailPageLoaded(ProductPageLoadedEvent $event)
{
$product = $event->getPage()->getProduct();
$this->addReviewExtension($product, $event);
$this->addAvailableOptions($event, $product);
}
private function addReviewExtension($product, $event) {
$reviewsArray = [];
/*
$call = $this->graphQLClient->getProductRating($product->getProductNumber());
if($call) {
$ratingObject = json_decode($call, true);
}
*/
$connection = \Shopware\Core\Kernel::getConnection();
// Get all master numbers for this product (could be master itself or find master via variant)
$masterNumbers = $connection->executeQuery("
SELECT DISTINCT masterNumber
FROM trusted_families
WHERE article = ? OR masterNumber = ?",
[$product->getProductNumber(), $product->getProductNumber()])
->fetchAllAssociative();
if (empty($masterNumbers)) {
// No family mapping exists - try direct match for internal reviews
$masterNumbers = [['masterNumber' => $product->getProductNumber()]];
}
$masterNumbersList = array_column($masterNumbers, 'masterNumber');
$placeholders = str_repeat('?,', count($masterNumbersList) - 1) . '?';
// Subquery to get distinct reviews first, then calculate average
$ratingObject = $connection->executeQuery("
SELECT round(avg(stars),2) as stars, count(*) as reviews
FROM (
SELECT DISTINCT r.id, r.stars
FROM `trusted_reviews` r
JOIN trusted_families f ON (f.masterNumber = r.article OR f.article = r.article)
WHERE f.masterNumber IN ($placeholders) OR f.article IN ($placeholders)
) as distinct_reviews",
array_merge($masterNumbersList, $masterNumbersList))
->fetchAssociative();
/* Double check, even if not necessary, on the existance of both stars and reviews */
if (isset($ratingObject['stars']) && isset($ratingObject['reviews']) && $ratingObject['reviews'] !== 0) {
$rating = [
'rating' => $ratingObject['stars'],
'totalReviews' => $ratingObject['reviews']
];
//$reviewsObject = json_decode($this->graphQLClient->getProductReviews($product->getProductNumber()), true);
$reviewsObject = $connection->executeQuery("
SELECT DISTINCT
r.id,
r.objectName as productName,
r.createdAt as creationDate,
r.stars as mark,
r.objectReview as comment,
r.reviewer_first_name,
r.reviewer_last_name_initial
FROM `trusted_reviews` r
JOIN trusted_families f ON (f.masterNumber = r.article OR f.article = r.article)
WHERE f.masterNumber IN ($placeholders) OR f.article IN ($placeholders)
ORDER BY r.createdAt DESC",
array_merge($masterNumbersList, $masterNumbersList))
->fetchAllAssociative();
/* Sorting by date */
usort($reviewsObject, function ($a, $b) {
return $a['creationDate'] <= $b['creationDate'];
});
/* We don't need all the data in this response. So we create a custom array with date, name of the reviewer, rating and review comment */
foreach ($reviewsObject as $review) {
$reviewer = null;
$comment = null;
// Format reviewer name: "Firstname L." from new fields
if (!empty($review['reviewer_first_name'])) {
$reviewer = $review['reviewer_first_name'];
if (!empty($review['reviewer_last_name_initial'])) {
$reviewer .= ' ' . $review['reviewer_last_name_initial'] . '.';
}
}
if (isset($review['comment'])) {
$comment = $review['comment'];
}
$reviewsArray[] = [
'productName' => $review['productName'], // Variant/Product name
'date' => $review['creationDate'],
'reviewer' => $reviewer,
'stars' => $review['mark'],
'comment' => $comment
];
}
$product->addExtension('rating', new ArrayStruct($rating));
$product->addExtension('reviews', new ArrayStruct($reviewsArray));
}
}
private function addAvailableOptions($event, $product) {
$parentId = $product->getParentId();
$availableOptions = [];
if ($parentId) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('id', $parentId));
$criteria->addAssociation('children');
$parent = $this->productRepository->search($criteria, $event->getContext())->first();
foreach ($parent->getChildren()->getElements() as $variant) {
if ($variant->getAvailable()) {
foreach ($variant->getOptionIds() as $option) {
if (!in_array($option, $availableOptions)) {
$availableOptions[] = $option;
}
}
}
}
}
$product->addExtension('availableOptions', new ArrayStruct($availableOptions));
}
private function loadSpecialProperties(SalesChannelEntityLoadedEvent $event, $themeConfig, bool $isMultipleProducts, array $products): void
{
// property group id configured in theme config
$configPropertyGroupId = isset($themeConfig["specialProductAttributeGroup"]) ? $themeConfig["specialProductAttributeGroup"] : false;
if (!$configPropertyGroupId) {
return;
}
// collect all loaded propertyIds
$propertyIds = $this->collectPropertyIds($isMultipleProducts, $products);
// load properties
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter("id", $propertyIds));
$criteria->addFilter(new EqualsFilter("groupId", $configPropertyGroupId));
/** @var EntityCollection */
$result = $this->propertyGroupOptionsRepository->search($criteria, $event->getContext())->getEntities();
// get properties and add as entity extension
$this->addPropertyExtension($isMultipleProducts, $products, $result, "doncarneSpecialProperties");
}
private function loadCutImages(SalesChannelEntityLoadedEvent $event, $themeConfig, bool $isMultipleProducts, array $products): void
{
$configPropertyGroupId = isset($themeConfig["cutProductAttributeGroup"]) ? $themeConfig["cutProductAttributeGroup"] : false;
if (!$configPropertyGroupId) {
return;
}
// collect all loaded propertyIds
$propertyIds = $this->collectPropertyIds($isMultipleProducts, $products);
// fetch image
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter("id", $propertyIds));
$criteria->addFilter(new EqualsFilter("groupId", $configPropertyGroupId));
$criteria->addAssociation("media");
/** @var EntityCollection */
$result = $this->propertyGroupOptionsRepository->search($criteria, $event->getContext())->getEntities();
$this->addPropertyExtension($isMultipleProducts, $products, $result, "doncarneCutProperties");
}
/**
* Collects property ids of all products in array.
* Returns an array containing the unique values only.
*/
private function collectPropertyIds(bool $isMultipleProducts, array $products): array
{
$propertyIds = [];
if ($isMultipleProducts) {
foreach ($products as $product) {
if ($product->getPropertyIds()) {
$propertyIds = array_merge($propertyIds, $product->getPropertyIds());
}
}
$propertyIds = array_unique($propertyIds);
} else {
if ($products[0]->getPropertyIds()) {
$propertyIds = $products[0]->getPropertyIds();
}
}
return $propertyIds;
}
private function addPropertyExtension(bool $isMultipleProducts, array $products, EntityCollection $result, string $name): void
{
if ($isMultipleProducts) {
foreach ($products as $product) {
$productOptions = new ArrayStruct([]);
if ($product->getPropertyIds()) {
foreach ($product->getPropertyIds() as $propertyId) {
if ($result->get($propertyId)) {
$productOptions->set($propertyId, $result->get($propertyId));
}
}
}
$product->addExtension($name, $productOptions);
}
} else {
$products[0]->addExtension($name, new ArrayStruct($result->getElements()));
}
}
private function loadBadgeProperties(SalesChannelEntityLoadedEvent $event, $themeConfig, bool $isMultipleProducts, array $products): void
{
// property group id configured in theme config
$configPropertyGroupId = isset($themeConfig["productAttributeGroupBadge"]) ? $themeConfig["productAttributeGroupBadge"] : false;
if (!$configPropertyGroupId) {
return;
}
// collect all loaded propertyIds
$propertyIds = $this->collectPropertyIds($isMultipleProducts, $products);
// load properties
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter("id", $propertyIds));
$criteria->addFilter(new EqualsFilter("groupId", $configPropertyGroupId));
$criteria->addAssociation('media');
/** @var EntityCollection */
$result = $this->propertyGroupOptionsRepository->search($criteria, $event->getContext())->getEntities();
// get properties and add as entity extension
$this->addPropertyExtension($isMultipleProducts, $products, $result, "doncarneProductBadge");
}
}