custom/plugins/NetiNextStoreLocator/src/Storefront/Page/Store/Listing/StoreListingPageLoader.php line 220

Open in your IDE?
  1. <?php
  2. namespace NetInventors\NetiNextStoreLocator\Storefront\Page\Store\Listing;
  3. use NetInventors\NetiNextStoreLocator\Components\CmsPageRenderer;
  4. use NetInventors\NetiNextStoreLocator\Components\ContactForm\ContactForm;
  5. use NetInventors\NetiNextStoreLocator\Core\Content\Store\StoreDefinition;
  6. use NetInventors\NetiNextStoreLocator\Core\Content\Store\StoreEntity;
  7. use NetInventors\NetiNextStoreLocator\Service\PluginConfig;
  8. use NetInventors\NetiNextStoreLocator\Service\StoreFilterService;
  9. use NetInventors\NetiNextStoreLocator\Struct\StoreSelectState;
  10. use NetInventors\NetiNextStorePickup\Service\ContextService;
  11. use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Common\RepositoryIterator;
  14. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  15. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Grouping\FieldGrouping;
  21. use Shopware\Core\Framework\Plugin\PluginEntity;
  22. use Shopware\Core\System\Country\CountryEntity;
  23. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  24. use Shopware\Core\System\SystemConfig\SystemConfigService;
  25. use Shopware\Storefront\Page\GenericPageLoader;
  26. use Shopware\Storefront\Page\MetaInformation;
  27. use Symfony\Component\HttpFoundation\Request;
  28. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  29. use Symfony\Contracts\Translation\TranslatorInterface;
  30. /**
  31.  * @psalm-suppress UndefinedClass The class is only available when StorePickup is installed.
  32.  */
  33. class StoreListingPageLoader
  34. {
  35.     /**
  36.      * @var GenericPageLoader
  37.      */
  38.     private $genericLoader;
  39.     /**
  40.      * @var EntityRepositoryInterface
  41.      */
  42.     private $storeRepository;
  43.     /**
  44.      * @var SystemConfigService
  45.      */
  46.     private $systemConfig;
  47.     /**
  48.      * @var ContactForm
  49.      */
  50.     private $contactForm;
  51.     /**
  52.      * @var EntityRepositoryInterface
  53.      */
  54.     private $mediaRepository;
  55.     /**
  56.      * @var CmsPageRenderer
  57.      */
  58.     private $cmsPageRenderer;
  59.     /**
  60.      * @var EventDispatcherInterface
  61.      */
  62.     private $eventDispatcher;
  63.     /**
  64.      * @var TranslatorInterface
  65.      */
  66.     private                           $translator;
  67.     private EntityRepositoryInterface $pluginRepository;
  68.     private StoreFilterService        $storeFilterService;
  69.     /**
  70.      * @var ContextService|null
  71.      */
  72.     private ?ContextService                   $contextService;
  73.     private SeoUrlPlaceholderHandlerInterface $seoUrlReplacer;
  74.     private string                            $shopwareVersion;
  75.     /**
  76.      * @param GenericPageLoader         $genericLoader
  77.      * @param EntityRepositoryInterface $storeRepository
  78.      * @param SystemConfigService       $systemConfig
  79.      * @param ContactForm               $contactForm
  80.      * @param EntityRepositoryInterface $mediaRepository
  81.      * @param CmsPageRenderer           $cmsPageRenderer
  82.      * @param EventDispatcherInterface  $eventDispatcher
  83.      * @param TranslatorInterface       $translator
  84.      * @param EntityRepositoryInterface $pluginRepository
  85.      * @param StoreFilterService        $storeFilterService
  86.      * @param ContextService|null       $contextService
  87.      *
  88.      * @psalm-suppress UndefinedClass The class is only available when StorePickup is installed.
  89.      */
  90.     public function __construct(
  91.         GenericPageLoader                 $genericLoader,
  92.         EntityRepositoryInterface         $storeRepository,
  93.         SystemConfigService               $systemConfig,
  94.         ContactForm                       $contactForm,
  95.         EntityRepositoryInterface         $mediaRepository,
  96.         CmsPageRenderer                   $cmsPageRenderer,
  97.         EventDispatcherInterface          $eventDispatcher,
  98.         TranslatorInterface               $translator,
  99.         EntityRepositoryInterface         $pluginRepository,
  100.         StoreFilterService                $storeFilterService,
  101.         ?ContextService                   $contextService,
  102.         SeoUrlPlaceholderHandlerInterface $seoUrlReplacer,
  103.         string                            $shopwareVersion
  104.     ) {
  105.         $this->genericLoader      $genericLoader;
  106.         $this->storeRepository    $storeRepository;
  107.         $this->systemConfig       $systemConfig;
  108.         $this->contactForm        $contactForm;
  109.         $this->mediaRepository    $mediaRepository;
  110.         $this->cmsPageRenderer    $cmsPageRenderer;
  111.         $this->eventDispatcher    $eventDispatcher;
  112.         $this->translator         $translator;
  113.         $this->pluginRepository   $pluginRepository;
  114.         $this->storeFilterService $storeFilterService;
  115.         $this->contextService     $contextService;
  116.         $this->seoUrlReplacer     $seoUrlReplacer;
  117.         $this->shopwareVersion    $shopwareVersion;
  118.     }
  119.     public function load(Request $requestSalesChannelContext $context): StoreListingPage
  120.     {
  121.         $page $this->genericLoader->load($request$context);
  122.         /** @var StoreListingPage $page */
  123.         $page StoreListingPage::createFrom($page);
  124.         $meta $page->getMetaInformation();
  125.         /** @var array<string, string> $config */
  126.         $config $this->getConfig($context);
  127.         $page->setConfig($config);
  128.         if ($meta instanceof MetaInformation) {
  129.             $meta->setMetaTitle(
  130.                 $this->translator->trans('neti-next-store-locator.index.title')
  131.             );
  132.             $meta->setMetaDescription(
  133.                 $this->translator->trans('neti-next-store-locator.index.description')
  134.             );
  135.             $seoUrl $config['seoUrl'];
  136.             if ('' !== $seoUrl) {
  137.                 $storefrontUrl = (string)$request->attributes->get('sw-storefront-url');
  138.                 $meta->assign([ 'canonical' => $storefrontUrl '/' $config['seoUrl'] ]);
  139.             } else {
  140.                 $meta->assign([ 'canonical' => $this->seoUrlReplacer->generate('frontend.store_locator.index') ]);
  141.             }
  142.         }
  143.         $countries $this->getCountries($context$config);
  144.         $page->setCountries($countries);
  145.         $stores $this->getStores($context);
  146.         $page->setStores($stores);
  147.         $filters $this->storeFilterService->loadFiltersForStorefront($stores$context);
  148.         $page->setFilters($filters);
  149.         $radiusList $this->getRadiusList();
  150.         $page->setRadiusList($radiusList);
  151.         $contactFormFields $this->contactForm->getFields($context);
  152.         $page->setContactFormFields($contactFormFields);
  153.         $contactSubjectOptions $this->getContactSubjectOptions($config);
  154.         $page->setContactSubjectOptions($contactSubjectOptions);
  155.         $page->setOrderTypes(
  156.             [
  157.                 'distance',
  158.                 'country',
  159.                 'name',
  160.                 'random',
  161.             ]
  162.         );
  163.         if (isset($config['topCmsPage']) && '' !== $config['topCmsPage']) {
  164.             $page->setTopCmsPageHtml(
  165.                 $this->cmsPageRenderer->buildById($request$context$config['topCmsPage'], compact('page'))
  166.             );
  167.         }
  168.         if (isset($config['bottomCmsPage']) && '' !== $config['bottomCmsPage']) {
  169.             $page->setBottomCmsPageHtml(
  170.                 $this->cmsPageRenderer->buildById($request$context$config['bottomCmsPage'], compact('page'))
  171.             );
  172.         }
  173.         $this->eventDispatcher->dispatch(new StoreListingPageLoadedEvent($page$context$request));
  174.         return $page;
  175.     }
  176.     /**
  177.      * Returns a list of all used countries
  178.      *
  179.      * @param SalesChannelContext $context
  180.      * @param array               $config
  181.      *
  182.      * @return array
  183.      * @throws \Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException
  184.      */
  185.     public function getCountries(SalesChannelContext $context, array $config)
  186.     {
  187.         $criteria = new Criteria();
  188.         $criteria->addAssociation('country');
  189.         $criteria->addGroupField(new FieldGrouping('countryId'));
  190.         $result    $this->storeRepository->search($criteria$context->getContext());
  191.         $countries = [
  192.             [
  193.                 'id'       => 'all',
  194.                 'label'    => $this->translator->trans('neti-next-store-locator.index.search.allCountriesLabel'),
  195.                 '_label'   => $this->translator->trans('neti-next-store-locator.index.search.allCountriesLabel'),
  196.                 'isoCode'  => 'ALL',
  197.                 'default'  => false,
  198.                 'position' => -1,
  199.             ],
  200.         ];
  201.         $workingIndex         1;
  202.         $defaultByConfigIndex null;
  203.         $umlautInput  = [ 'ä''Ä''ö''Ö''ü''Ü' ];
  204.         $umlautOutput = [ 'ae''Ae''oe''Oe''ue''Ue' ];
  205.         /** @var StoreEntity $entity */
  206.         foreach ($result as $entity) {
  207.             $country $entity->getCountry();
  208.             if (!($country instanceof CountryEntity)) {
  209.                 continue;
  210.             }
  211.             if (null === $defaultByConfigIndex && $country->getId() === ($config['preselectedCountryId'] ?? null)) {
  212.                 $defaultByConfigIndex $workingIndex;
  213.             }
  214.             $countries[$workingIndex++] = [
  215.                 'id'       => $country->getId(),
  216.                 'label'    => $country->getTranslated()['name'],
  217.                 '_label'   => str_replace($umlautInput$umlautOutput$country->getTranslated()['name']),
  218.                 'isoCode'  => $country->getIso(),
  219.                 'default'  => false,
  220.                 'position' => $country->getPosition(),
  221.             ];
  222.         }
  223.         $defaultIndex $defaultByConfigIndex ?? 0;
  224.         $countries[$defaultIndex]['default'] = true;
  225.         $sortBy $config['countrySortBy'] ?? PluginConfig::COUNTRY_SORT_BY_NAME_ASC;
  226.         usort($countries, function ($a$b) use ($sortBy) {
  227.             if ($a['id'] === 'all') {
  228.                 return -1;
  229.             }
  230.             switch ($sortBy) {
  231.                 case PluginConfig::COUNTRY_SORT_BY_NAME_ASC:
  232.                     return $a['_label'] > $b['_label'];
  233.                 case PluginConfig::COUNTRY_SORT_BY_NAME_DESC:
  234.                     return $a['_label'] < $b['_label'];
  235.                 case PluginConfig::COUNTRY_SORT_BY_POSITION_ASC;
  236.                     return $a['position'] > $b['position'];
  237.                 case PluginConfig::COUNTRY_SORT_BY_POSITION_DESC:
  238.                     return $a['position'] < $b['position'];
  239.             }
  240.         });
  241.         return $countries;
  242.     }
  243.     /**
  244.      * Returns a list of stores.
  245.      *
  246.      * @param SalesChannelContext $context
  247.      *
  248.      * @return \Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult
  249.      * @throws \Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException
  250.      */
  251.     public function getStores(SalesChannelContext $context)
  252.     {
  253.         $criteria = new Criteria();
  254.         $criteria->addAssociation('country');
  255.         $criteria->addAssociation('countryState');
  256.         $criteria->addAssociation('pictureMedia');
  257.         $criteria->addAssociation('iconMedia');
  258.         $criteria->addAssociation('translation');
  259.         $criteria->addAssociation('tags');
  260.         $criteria->addFilter(new EqualsFilter('active'true));
  261.         $criteria->addFilter(new EqualsFilter('salesChannels.id'$context->getSalesChannel()->getId()));
  262.         $criteria->addFilter(
  263.             new NotFilter(
  264.                 NotFilter::CONNECTION_OR,
  265.                 [
  266.                     new EqualsFilter('latitude'null),
  267.                     new EqualsFilter('longitude'null),
  268.                 ]
  269.             )
  270.         );
  271.         $iterator = new RepositoryIterator($this->storeRepository$context->getContext(), $criteria);
  272.         $result   null;
  273.         if (!$iterator->getTotal()) {
  274.             return new EntitySearchResult(
  275.                 StoreDefinition::ENTITY_NAME,
  276.                 0,
  277.                 new EntityCollection(),
  278.                 null,
  279.                 $criteria,
  280.                 $context->getContext()
  281.             );
  282.         }
  283.         while ($rows $iterator->fetch()) {
  284.             if (null === $result) {
  285.                 $result $rows;
  286.             } else {
  287.                 foreach ($rows as $row) {
  288.                     $result->add($row);
  289.                 }
  290.             }
  291.         }
  292.         $detailMode      $this->systemConfig->get('NetiNextStoreLocator.config.detailPage');
  293.         $selectedStoreId null;
  294.         if (
  295.             null !== $this->contextService
  296.             && $this->isStorePickupEnabled($context->getContext())
  297.         ) {
  298.             /**
  299.              * @psalm-suppress UndefinedClass The class is only available when StorePickup is installed.
  300.              */
  301.             $selectedStoreId $this->contextService->getSelectedStore();
  302.         }
  303.         /** @var StoreEntity $entity */
  304.         foreach ($result as $entity) {
  305.             if ($entity->getId() === $selectedStoreId) {
  306.                 $entity->addExtension('netiStorePickupSelected', new StoreSelectState());
  307.             }
  308.             switch ($detailMode) {
  309.                 case 'enabled':
  310.                     $entity->setDetailPageEnabled(true);
  311.                     break;
  312.                 case 'disabled':
  313.                     $entity->setDetailPageEnabled(false);
  314.                     break;
  315.                 case 'store':
  316.                     // Keep value
  317.                     break;
  318.             }
  319.         }
  320.         return $result;
  321.     }
  322.     private function getRadiusList(): array
  323.     {
  324.         $values       $this->systemConfig->get('NetiNextStoreLocator.config.searchRadiusValues');
  325.         $defaultValue $this->systemConfig->get('NetiNextStoreLocator.config.defaultSearchRadius');
  326.         $values       trim($values);
  327.         if (empty($values)) {
  328.             return [];
  329.         }
  330.         $values array_unique(explode(';'trim($values'; ')));
  331.         return array_map(
  332.             function ($value) use ($defaultValue) {
  333.                 return [
  334.                     'default' => (int)$value === (int)$defaultValue,
  335.                     'value'   => $value,
  336.                 ];
  337.             },
  338.             $values
  339.         );
  340.     }
  341.     public function getConfig(SalesChannelContext $context): array
  342.     {
  343.         $config = (array)$this->systemConfig->get(PluginConfig::CONFIG_DOMAIN$context->getSalesChannel()->getId());
  344.         foreach ($config as $key => $value) {
  345.             unset ($config[$key]);
  346.             switch ($key) {
  347.                 case 'googleMapIcon':
  348.                     $mediaId $value;
  349.                     if (!empty($mediaId)) {
  350.                         /**
  351.                          * @psalm-suppress MixedArgumentTypeCoercion
  352.                          *
  353.                          * This is the correct way to search for a specific ID
  354.                          */
  355.                         $criteria = new Criteria([ $mediaId ]);
  356.                         $result   $this->mediaRepository->search($criteria$context->getContext());
  357.                         if ($result->count() > 0) {
  358.                             $value $result->first()->getUrl();
  359.                         } else {
  360.                             throw new \Exception('The given mediaId does not exist.');
  361.                         }
  362.                     }
  363.                     break;
  364.                 case 'googleMapIconSize':
  365.                     if (!empty($value)) {
  366.                         if (!preg_match('/^([0-9]+)x([0-9]+)$/'$value)) {
  367.                             throw new \Exception('The given icon size "' $value '" is invalid.');
  368.                         }
  369.                         [ $width$height ] = array_map('intval'explode('x'$value));
  370.                         $value compact('width''height');
  371.                     }
  372.                     break;
  373.             }
  374.             $config[$key] = $value;
  375.         }
  376.         $config['_storePickupEnabled']   = $this->isStorePickupEnabled($context->getContext());
  377.         $config['_cookieConsentEnabled'] = version_compare($this->shopwareVersion'6.4.12.0''<')
  378.             ?: $this->systemConfig->get('core.basicInformation.useDefaultCookieConsent');
  379.         return $config;
  380.     }
  381.     private function getContactSubjectOptions(array $config): array
  382.     {
  383.         $options $config['contactSubjectOptions'] ?? '';
  384.         $options trim($options);
  385.         if (empty($options)) {
  386.             return [];
  387.         }
  388.         return explode(PHP_EOL$options);
  389.     }
  390.     private function isStorePickupEnabled(Context $context): bool
  391.     {
  392.         $criteria = new Criteria();
  393.         $criteria->addFilter(new EqualsFilter('name''NetiNextStorePickup'));
  394.         $result $this->pluginRepository->search($criteria$context);
  395.         $plugin $result->first();
  396.         return $plugin instanceof PluginEntity
  397.             && $plugin->getInstalledAt() !== null
  398.             && $plugin->getActive() === true;
  399.     }
  400. }