custom/plugins/NetiNextStoreLocator/src/Subscriber/CheckoutFinishPageSubscriber.php line 65

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace NetInventors\NetiNextStoreLocator\Subscriber;
  4. use Doctrine\DBAL\Connection;
  5. use NetInventors\NetiNextStoreLocator\Components\GeoLocation\GeoLocation;
  6. use NetInventors\NetiNextStoreLocator\Components\GeoLocation\Struct\Address;
  7. use NetInventors\NetiNextStoreLocator\Components\GeoLocation\Struct\Coordinates;
  8. use NetInventors\NetiNextStoreLocator\Core\Content\Store\StoreEntity;
  9. use NetInventors\NetiNextStoreLocator\Service\PluginConfig;
  10. use NetInventors\NetiNextStoreLocator\Struct\CheckoutFinishStoresStruct;
  11. use NetInventors\NetiNextStoreLocator\Struct\PluginConfigStruct;
  12. use NetInventors\NetiNextStoreLocator\Struct\StoreDistanceStruct;
  13. use Psr\Cache\CacheItemPoolInterface;
  14. use Psr\Log\LoggerInterface;
  15. use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryCollection;
  16. use Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryEntity;
  17. use Shopware\Core\Checkout\Order\OrderEntity;
  18. use Shopware\Core\Framework\Context;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  21. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  22. use Shopware\Storefront\Page\Checkout\Finish\CheckoutFinishPageLoadedEvent;
  23. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  24. class CheckoutFinishPageSubscriber implements EventSubscriberInterface
  25. {
  26.     private PluginConfigStruct        $pluginConfig;
  27.     private GeoLocation               $geoLocation;
  28.     private Connection                $connection;
  29.     private EntityRepositoryInterface $storeRepository;
  30.     private LoggerInterface           $logger;
  31.     private CacheItemPoolInterface    $cache;
  32.     public function __construct(
  33.         PluginConfigStruct        $pluginConfig,
  34.         GeoLocation               $geoLocation,
  35.         Connection                $connection,
  36.         EntityRepositoryInterface $storeRepository,
  37.         LoggerInterface           $logger,
  38.         CacheItemPoolInterface    $cache
  39.     ) {
  40.         $this->pluginConfig    $pluginConfig;
  41.         $this->geoLocation     $geoLocation;
  42.         $this->connection      $connection;
  43.         $this->storeRepository $storeRepository;
  44.         $this->logger          $logger;
  45.         $this->cache           $cache;
  46.     }
  47.     public static function getSubscribedEvents(): array
  48.     {
  49.         return [
  50.             CheckoutFinishPageLoadedEvent::class => 'onCheckoutFinishPageLoaded',
  51.         ];
  52.     }
  53.     public function onCheckoutFinishPageLoaded(CheckoutFinishPageLoadedEvent $event): void
  54.     {
  55.         if (
  56.             false === $this->pluginConfig->isActive()
  57.             || false === $this->pluginConfig->isShowStoresOnOrderFinish()
  58.         ) {
  59.             return;
  60.         }
  61.         $page  $event->getPage();
  62.         $order $page->getOrder();
  63.         try {
  64.             $coordinates $this->getCoordinates($order);
  65.             if ($coordinates instanceof Coordinates) {
  66.                 $stores $this->getNearestStores($coordinates$event->getSalesChannelContext());
  67.                 $struct = new CheckoutFinishStoresStruct($stores);
  68.                 $page->addExtension('netiStores'$struct);
  69.             }
  70.         } catch (\Exception $ex) {
  71.             $this->logger->error(
  72.                 'Unable to fetch nearest stores for the order finish page',
  73.                 [
  74.                     'message' => $ex->getMessage(),
  75.                 ]
  76.             );
  77.         }
  78.     }
  79.     private function getNearestStores(Coordinates $coordinatesSalesChannelContext $salesChannelContext): array
  80.     {
  81.         $context $salesChannelContext->getContext();
  82.         $limit   max(0$this->pluginConfig->getOrderFinishStoreCount());
  83.         $sql     "
  84.             SELECT
  85.               LOWER(HEX(s.id)) AS id,
  86.               (
  87.                 :unit * ACOS(COS(RADIANS(:lat)) * COS(RADIANS(s.latitude)) *
  88.                 COS(RADIANS(s.longitude) - RADIANS(:lng)) + SIN(RADIANS(:lat)) * SIN(RADIANS(s.latitude)))
  89.               ) AS distance
  90.             FROM neti_store_locator s
  91.             LEFT JOIN neti_store_sales_channel c ON c.store_id = s.id
  92.             WHERE active = 1
  93.               AND HEX(c.sales_channel_id) = :salesChannelId
  94.               AND s.latitude IS NOT NULL
  95.               AND s.longitude IS NOT NULL
  96.             ORDER BY distance ASC
  97.             LIMIT $limit
  98.         ";
  99.         $stores $this->connection->fetchAllKeyValue(
  100.             $sql,
  101.             [
  102.                 'unit'           => $this->pluginConfig->getDistanceUnit() === 'km' 6371 3959,
  103.                 'lat'            => $coordinates->getLatitude(),
  104.                 'lng'            => $coordinates->getLongitude(),
  105.                 'salesChannelId' => $salesChannelContext->getSalesChannelId(),
  106.             ]
  107.         );
  108.         /**
  109.          * @psalm-suppress MixedArgumentTypeCoercion
  110.          *
  111.          * This is the correct way to search for a IDs
  112.          */
  113.         $criteria = new Criteria(array_keys($stores));
  114.         $result   $this->storeRepository->search($criteria$context)->getElements();
  115.         /** @var StoreEntity $store */
  116.         foreach ($result as $store) {
  117.             $distanceStruct = new StoreDistanceStruct((float)$stores[$store->getId()]);
  118.             $store->addExtension('netiDistance'$distanceStruct);
  119.         }
  120.         return $result;
  121.     }
  122.     private function getCoordinates(OrderEntity $order): ?Coordinates
  123.     {
  124.         $address null;
  125.         if ($this->pluginConfig->getOrderFinishAddressType() === PluginConfigStruct::ORDER_FINISH_ADDRESS_TYPE_SHIPPING) {
  126.             $delivery   null;
  127.             $deliveries $order->getDeliveries();
  128.             if ($deliveries instanceof OrderDeliveryCollection) {
  129.                 /** @var null|OrderDeliveryEntity $delivery */
  130.                 $delivery $deliveries->first();
  131.             }
  132.             if ($delivery instanceof OrderDeliveryEntity) {
  133.                 $address $delivery->getShippingOrderAddress();
  134.             }
  135.         } else {
  136.             $address $order->getBillingAddress();
  137.         }
  138.         if (null === $address) {
  139.             return null;
  140.         }
  141.         $cacheKey 'neti_store_locator_address_coordinates_' $address->getId();
  142.         $item     $this->cache->getItem($cacheKey);
  143.         if ($item->isHit()) {
  144.             /** @var Coordinates|mixed $coordinates */
  145.             $coordinates $item->get();
  146.             if ($coordinates instanceof Coordinates) {
  147.                 return $coordinates;
  148.             }
  149.         }
  150.         $addressStruct = new Address();
  151.         $addressStruct->setStreet($address->getStreet());
  152.         $addressStruct->setZipCode($address->getZipcode());
  153.         $addressStruct->setCity($address->getCity());
  154.         $addressStruct->setCountryId($address->getCountryId());
  155.         $coordinates $this->geoLocation->getCoords($addressStruct);
  156.         $item->set($coordinates);
  157.         $this->cache->save($item);
  158.         return $coordinates;
  159.     }
  160. }