custom/plugins/theme/src/Subscriber/ProductSubscriber.php line 71

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace DonCarneTheme\Subscriber;
  3. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  4. use Shopware\Core\Content\Product\ProductEvents;
  5. use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
  6. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  7. use Shopware\Core\System\SystemConfig\SystemConfigService;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  12. use Shopware\Core\Framework\Struct\ArrayStruct;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  14. use DonCarneTheme\Service\GraphQLClient;
  15. class ProductSubscriber implements EventSubscriberInterface
  16. {
  17.     private SystemConfigService $systemConfigService;
  18.     private EntityRepository $propertyGroupOptionsRepository;
  19.     private EntityRepository $productRepository;
  20.     private GraphQLClient $graphQLClient;
  21.     public function __construct(
  22.         SystemConfigService $systemConfigService,
  23.         EntityRepository $propertyGroupOptionsRepository,
  24.         EntityRepository $productRepository,
  25.         GraphQLClient $graphQLClient
  26.     ) {
  27.         $this->systemConfigService $systemConfigService;
  28.         $this->propertyGroupOptionsRepository $propertyGroupOptionsRepository;
  29.         $this->productRepository $productRepository;
  30.         $this->graphQLClient $graphQLClient;
  31.     }
  32.     public static function getSubscribedEvents(): array
  33.     {
  34.         return [
  35.             'sales_channel.' ProductEvents::PRODUCT_LOADED_EVENT => 'onProductLoaded',
  36.             ProductPageLoadedEvent::class => 'onProductDetailPageLoaded'
  37.         ];
  38.     }
  39.     public function onProductLoaded(SalesChannelEntityLoadedEvent $event)
  40.     {
  41.         $products $event->getEntities();
  42.         if (empty($products)) {
  43.             return;
  44.         }
  45.         $isMultipleProducts count($products) > true false;
  46.         $themeConfig $this->systemConfigService->get("DonCarneTheme.config");
  47.         $this->loadSpecialProperties($event$themeConfig$isMultipleProducts$products);
  48.         $this->loadCutImages($event$themeConfig$isMultipleProducts$products);
  49.         $this->loadBadgeProperties($event$themeConfig$isMultipleProducts$products);
  50.     }
  51.     public function onProductDetailPageLoaded(ProductPageLoadedEvent $event)
  52.     {
  53.         $product $event->getPage()->getProduct();
  54.         $this->addReviewExtension($product$event);
  55.         $this->addAvailableOptions($event$product);
  56.     }
  57.     private function addReviewExtension($product$event) {
  58.         $reviewsArray = [];
  59.         /*
  60.         $call = $this->graphQLClient->getProductRating($product->getProductNumber());
  61.         if($call) {
  62.             $ratingObject = json_decode($call, true);
  63.         }
  64.         */
  65.         $connection \Shopware\Core\Kernel::getConnection();
  66.         // Get all master numbers for this product (could be master itself or find master via variant)
  67.         $masterNumbers $connection->executeQuery("
  68.             SELECT DISTINCT masterNumber
  69.             FROM trusted_families
  70.             WHERE article = ? OR masterNumber = ?",
  71.             [$product->getProductNumber(), $product->getProductNumber()])
  72.             ->fetchAllAssociative();
  73.         if (empty($masterNumbers)) {
  74.             // No family mapping exists - try direct match for internal reviews
  75.             $masterNumbers = [['masterNumber' => $product->getProductNumber()]];
  76.         }
  77.         $masterNumbersList array_column($masterNumbers'masterNumber');
  78.         $placeholders str_repeat('?,'count($masterNumbersList) - 1) . '?';
  79.         // Subquery to get distinct reviews first, then calculate average
  80.         $ratingObject $connection->executeQuery("
  81.             SELECT round(avg(stars),2) as stars, count(*) as reviews
  82.             FROM (
  83.                 SELECT DISTINCT r.id, r.stars
  84.                 FROM `trusted_reviews` r
  85.                 JOIN trusted_families f ON (f.masterNumber = r.article OR f.article = r.article)
  86.                 WHERE f.masterNumber IN ($placeholders) OR f.article IN ($placeholders)
  87.             ) as distinct_reviews",
  88.             array_merge($masterNumbersList$masterNumbersList))
  89.             ->fetchAssociative();
  90.         /* Double check, even if not necessary, on the existance of both stars and reviews */
  91.         if (isset($ratingObject['stars']) && isset($ratingObject['reviews']) && $ratingObject['reviews'] !== 0) {
  92.             $rating = [
  93.                 'rating' => $ratingObject['stars'],
  94.                 'totalReviews' => $ratingObject['reviews']
  95.             ];
  96.             //$reviewsObject = json_decode($this->graphQLClient->getProductReviews($product->getProductNumber()), true);
  97.             $reviewsObject $connection->executeQuery("
  98.                 SELECT DISTINCT
  99.                     r.id,
  100.                     r.objectName as productName,
  101.                     r.createdAt as creationDate,
  102.                     r.stars as mark,
  103.                     r.objectReview as comment,
  104.                     r.reviewer_first_name,
  105.                     r.reviewer_last_name_initial
  106.                 FROM `trusted_reviews` r
  107.                 JOIN trusted_families f ON (f.masterNumber = r.article OR f.article = r.article)
  108.                 WHERE f.masterNumber IN ($placeholders) OR f.article IN ($placeholders)
  109.                 ORDER BY r.createdAt DESC",
  110.                 array_merge($masterNumbersList$masterNumbersList))
  111.                 ->fetchAllAssociative();
  112.             /* Sorting by date */
  113.             usort($reviewsObject, function ($a$b) {
  114.                 return $a['creationDate'] <= $b['creationDate'];
  115.             });
  116.             /* 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 */
  117.             foreach ($reviewsObject as $review) {
  118.                 $reviewer null;
  119.                 $comment null;
  120.                 // Format reviewer name: "Firstname L." from new fields
  121.                 if (!empty($review['reviewer_first_name'])) {
  122.                     $reviewer $review['reviewer_first_name'];
  123.                     if (!empty($review['reviewer_last_name_initial'])) {
  124.                         $reviewer .= ' ' $review['reviewer_last_name_initial'] . '.';
  125.                     }
  126.                 }
  127.                 if (isset($review['comment'])) {
  128.                     $comment $review['comment'];
  129.                 }
  130.                 $reviewsArray[] = [
  131.                     'productName' => $review['productName'], // Variant/Product name
  132.                     'date' => $review['creationDate'],
  133.                     'reviewer' => $reviewer,
  134.                     'stars' => $review['mark'],
  135.                     'comment' => $comment
  136.                 ];
  137.             }
  138.             
  139.             $product->addExtension('rating', new ArrayStruct($rating));
  140.             $product->addExtension('reviews', new ArrayStruct($reviewsArray));
  141.         }
  142.         
  143.     }
  144.     private function addAvailableOptions($event$product) {
  145.         $parentId $product->getParentId();
  146.         $availableOptions = [];
  147.         if ($parentId) {
  148.             $criteria = new Criteria();
  149.             $criteria->addFilter(new EqualsFilter('id'$parentId));
  150.             $criteria->addAssociation('children');
  151.             $parent $this->productRepository->search($criteria$event->getContext())->first();
  152.             foreach ($parent->getChildren()->getElements() as $variant) {
  153.                 if ($variant->getAvailable()) {
  154.                     foreach ($variant->getOptionIds() as $option) {
  155.                         if (!in_array($option$availableOptions)) {
  156.                             $availableOptions[] = $option;
  157.                         }
  158.                     }
  159.                 }
  160.             }
  161.         }
  162.         $product->addExtension('availableOptions', new ArrayStruct($availableOptions));
  163.     }
  164.     private function loadSpecialProperties(SalesChannelEntityLoadedEvent $event$themeConfigbool $isMultipleProducts, array $products): void
  165.     {
  166.         // property group id configured in theme config
  167.         $configPropertyGroupId = isset($themeConfig["specialProductAttributeGroup"]) ? $themeConfig["specialProductAttributeGroup"] : false;
  168.         if (!$configPropertyGroupId) {
  169.             return;
  170.         }
  171.         // collect all loaded propertyIds
  172.         $propertyIds $this->collectPropertyIds($isMultipleProducts$products);
  173.         // load properties
  174.         $criteria = new Criteria();
  175.         $criteria->addFilter(new EqualsAnyFilter("id"$propertyIds));
  176.         $criteria->addFilter(new EqualsFilter("groupId"$configPropertyGroupId));
  177.         /** @var EntityCollection */
  178.         $result $this->propertyGroupOptionsRepository->search($criteria$event->getContext())->getEntities();
  179.         // get properties and add as entity extension
  180.         $this->addPropertyExtension($isMultipleProducts$products$result"doncarneSpecialProperties");
  181.     }
  182.     private function loadCutImages(SalesChannelEntityLoadedEvent $event$themeConfigbool $isMultipleProducts, array $products): void
  183.     {
  184.         $configPropertyGroupId = isset($themeConfig["cutProductAttributeGroup"]) ? $themeConfig["cutProductAttributeGroup"] : false;
  185.         if (!$configPropertyGroupId) {
  186.             return;
  187.         }
  188.         // collect all loaded propertyIds
  189.         $propertyIds $this->collectPropertyIds($isMultipleProducts$products);
  190.         // fetch image
  191.         $criteria = new Criteria();
  192.         $criteria->addFilter(new EqualsAnyFilter("id"$propertyIds));
  193.         $criteria->addFilter(new EqualsFilter("groupId"$configPropertyGroupId));
  194.         $criteria->addAssociation("media");
  195.         /** @var EntityCollection */
  196.         $result $this->propertyGroupOptionsRepository->search($criteria$event->getContext())->getEntities();
  197.         $this->addPropertyExtension($isMultipleProducts$products$result"doncarneCutProperties");
  198.     }
  199.     /**
  200.      * Collects property ids of all products in array.
  201.      * Returns an array containing the unique values only.
  202.      */
  203.     private function collectPropertyIds(bool $isMultipleProducts, array $products): array
  204.     {
  205.         $propertyIds = [];
  206.         if ($isMultipleProducts) {
  207.             foreach ($products as $product) {
  208.                 if ($product->getPropertyIds()) {
  209.                     $propertyIds array_merge($propertyIds$product->getPropertyIds());
  210.                 }
  211.             }
  212.             $propertyIds array_unique($propertyIds);
  213.         } else {
  214.             if ($products[0]->getPropertyIds()) {
  215.                 $propertyIds $products[0]->getPropertyIds();
  216.             }
  217.         }
  218.         return $propertyIds;
  219.     }
  220.     private function addPropertyExtension(bool $isMultipleProducts, array $productsEntityCollection $resultstring $name): void
  221.     {
  222.         if ($isMultipleProducts) {
  223.             foreach ($products as $product) {
  224.                 $productOptions = new ArrayStruct([]);
  225.                 if ($product->getPropertyIds()) {
  226.                     foreach ($product->getPropertyIds() as $propertyId) {
  227.                         if ($result->get($propertyId)) {
  228.                             $productOptions->set($propertyId$result->get($propertyId));
  229.                         }
  230.                     }
  231.                 }
  232.                 $product->addExtension($name$productOptions);
  233.             }
  234.         } else {
  235.             $products[0]->addExtension($name, new ArrayStruct($result->getElements()));
  236.         }
  237.     }
  238.     private function loadBadgeProperties(SalesChannelEntityLoadedEvent $event$themeConfigbool $isMultipleProducts, array $products): void
  239.     {
  240.         // property group id configured in theme config
  241.         $configPropertyGroupId = isset($themeConfig["productAttributeGroupBadge"]) ? $themeConfig["productAttributeGroupBadge"] : false;
  242.         if (!$configPropertyGroupId) {
  243.             return;
  244.         }
  245.         // collect all loaded propertyIds
  246.         $propertyIds $this->collectPropertyIds($isMultipleProducts$products);
  247.         // load properties
  248.         $criteria = new Criteria();
  249.         $criteria->addFilter(new EqualsAnyFilter("id"$propertyIds));
  250.         $criteria->addFilter(new EqualsFilter("groupId"$configPropertyGroupId));
  251.         $criteria->addAssociation('media');
  252.         /** @var EntityCollection */
  253.         $result $this->propertyGroupOptionsRepository->search($criteria$event->getContext())->getEntities();
  254.         // get properties and add as entity extension
  255.         $this->addPropertyExtension($isMultipleProducts$products$result"doncarneProductBadge");
  256.     }
  257. }