src/EventSubscriber/SitemapSubscriber.php line 103

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-04-18
  5.  * Time: 11:17
  6.  */
  7. namespace App\EventSubscriber;
  8. use App\Entity\EnumTrait;
  9. use App\Entity\Location\City;
  10. use App\Entity\Profile\Profile;
  11. use App\Entity\Profile\ProfileInventoryItem;
  12. use App\Entity\Profile\ProfileInventoryItemType;
  13. use App\Entity\Service;
  14. use App\Entity\Saloon\Saloon;
  15. use App\Repository\CityRepository;
  16. use App\Repository\PageMetadataRepository;
  17. use App\Repository\ProfileInventoryItemRepository;
  18. use App\Repository\ProfileRepository;
  19. use App\Repository\SaloonRepository;
  20. use App\Repository\ServiceRepository;
  21. use App\Routing\DynamicRouter;
  22. use App\Service\Features;
  23. use App\Service\ProfileNameLandingProvider;
  24. use App\Service\SeoRouteTemplateMtimesProvider;
  25. use Carbon\Carbon;
  26. use Carbon\CarbonImmutable;
  27. use Doctrine\Persistence\ManagerRegistry;
  28. use GuzzleHttp\ClientInterface;
  29. use Presta\SitemapBundle\Event\SitemapPopulateEvent;
  30. use Presta\SitemapBundle\Service\UrlContainerInterface;
  31. use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
  32. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  33. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  34. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  35. use Symfony\Component\Routing\RouterInterface;
  36. use Symfony\Component\Yaml\Yaml;
  37. class SitemapSubscriber implements EventSubscriberInterface
  38. {
  39.     public const ROUTE_SITEMAP_OPTION = 'sitemap.custom';
  40.     protected CityRepository $cityRepository;
  41.     protected ProfileRepository $profileRepository;
  42.     protected ProfileInventoryItemRepository $profileInventoryItemRepository;
  43.     protected SaloonRepository $saloonRepository;
  44.     protected ServiceRepository $serviceRepository;
  45.     protected string $defaultCity;
  46.     protected ClientInterface $httpClient;
  47.     private array $sitemapConfig;
  48.     private ?array $routeLocales;
  49.     /** @var array<string, \DateTimeImmutable> */
  50.     private array $uriLastModifiedMap = [];
  51.     /** @var array<string, \DateTimeImmutable> */
  52.     private array $routeLastModifiedMap = [];
  53.     public function __construct(
  54.         protected RouterInterface $router,
  55.         private Features          $features,
  56.         ManagerRegistry           $registry,
  57.         ParameterBagInterface     $parameterBag,
  58.         ClientInterface           $apiDomainTimelineClient,
  59.         protected string          $sitemapConfigPath,
  60.         protected PageMetadataRepository $pageMetadataRepository,
  61.         protected SeoRouteTemplateMtimesProvider $seoRouteTemplateMtimesProvider,
  62.         private ProfileNameLandingProvider $profileNameLandingProvider,
  63.     )
  64.     {
  65.         $this->defaultCity = $parameterBag->get('default_city');
  66.         $this->cityRepository = $registry->getManagerForClass(City::class)->getRepository(City::class);
  67.         $this->profileRepository = $registry->getManagerForClass(Profile::class)->getRepository(Profile::class);
  68.         $this->profileInventoryItemRepository = $registry->getManagerForClass(ProfileInventoryItem::class)->getRepository(ProfileInventoryItem::class);
  69.         $this->saloonRepository = $registry->getManagerForClass(Saloon::class)->getRepository(Saloon::class);
  70.         $this->serviceRepository = $registry->getManagerForClass(Service::class)->getRepository(Service::class);
  71.         $this->httpClient = $apiDomainTimelineClient;
  72.         if ($this->features->has_translations()) {
  73.             $this->routeLocales = $this->features->sitemap_multiple_locales()
  74.                 ? ['ru', 'en']
  75.                 : ['ru'];
  76.         } else {
  77.             $this->routeLocales = null;
  78.         }
  79.     }
  80.     /**
  81.      * @inheritDoc
  82.      */
  83.     public static function getSubscribedEvents()
  84.     {
  85.         return [
  86.             SitemapPopulateEvent::ON_SITEMAP_POPULATE => 'populate',
  87.         ];
  88.     }
  89.     public function populate(SitemapPopulateEvent $event): void
  90.     {
  91.         $this->prepareConfig();
  92.         $this->uriLastModifiedMap = $this->pageMetadataRepository->findUriLastModifiedMap();
  93.         $this->routeLastModifiedMap = $this->seoRouteTemplateMtimesProvider->getRouteMtimes();
  94.         $urlContainer = $event->getUrlContainer();
  95.         $this->registerHomepage($urlContainer);
  96.         $this->registerCityUrls($urlContainer);
  97.         $this->registerProfileUrls($urlContainer);
  98.         if ($this->features->has_saloons()) {
  99.             $this->registerSaloonUrls($urlContainer);
  100.         }
  101.     }
  102.     private function dateMutable(?\DateTimeImmutable $dateImmutable): ?\DateTime
  103.     {
  104.         if (null === $dateImmutable) {
  105.             return Carbon::now();
  106.         }
  107.         return Carbon::createFromTimestampUTC($dateImmutable->getTimestamp());
  108.     }
  109.     private function normalizeUrlPath(string $absoluteUrl): string
  110.     {
  111.         $path = parse_url($absoluteUrl, PHP_URL_PATH) ?? '/';
  112.         $path = str_replace('/en', '', $path);
  113.         if (false !== $pos = strpos($path, '?')) {
  114.             $path = substr($path, 0, $pos);
  115.         }
  116.         return $path;
  117.     }
  118.     private function resolveListingLastModified(string $absoluteUrl, string $canonicalRoute, string $section): \DateTime
  119.     {
  120.         $path = $this->normalizeUrlPath($absoluteUrl);
  121.         if (isset($this->uriLastModifiedMap[$path])) {
  122.             return $this->dateMutable($this->uriLastModifiedMap[$path]);
  123.         }
  124.         if (isset($this->routeLastModifiedMap[$canonicalRoute])) {
  125.             return $this->dateMutable($this->routeLastModifiedMap[$canonicalRoute]);
  126.         }
  127.         return $this->getSectionLastModified($section);
  128.     }
  129.     private function addListingUrl(
  130.         UrlContainerInterface $urlContainer,
  131.         string $absoluteUrl,
  132.         string $canonicalRoute,
  133.         string $section,
  134.     ): void {
  135.         $urlContainer->addUrl(
  136.             new UrlConcrete($absoluteUrl, $this->resolveListingLastModified($absoluteUrl, $canonicalRoute, $section)),
  137.             $this->getSitemapSectionName($section)
  138.         );
  139.     }
  140.     private function generateLocalizedUrls(string $canonicalRoute, array $routeParameters): iterable
  141.     {
  142.         if (null === $this->routeLocales) {
  143.             yield $this->router->generate($canonicalRoute, $routeParameters, UrlGeneratorInterface::ABSOLUTE_URL);
  144.         } else {
  145.             foreach ($this->routeLocales as $routeLocale) {
  146.                 yield $this->router->generate("$canonicalRoute.$routeLocale", $routeParameters, UrlGeneratorInterface::ABSOLUTE_URL);
  147.             }
  148.         }
  149.     }
  150.     protected function registerHomepage(UrlContainerInterface $urlContainer): void
  151.     {
  152.         foreach ($this->generateLocalizedUrls('homepage', []) as $url) {
  153.             $this->addListingUrl($urlContainer, $url, 'homepage', 'geo');
  154.         }
  155.     }
  156.     protected function registerCityUrls(UrlContainerInterface $urlContainer): void
  157.     {
  158.         $homepageAsCityList = $this->features->homepage_as_city_list();
  159.         foreach ($this->cityRepository->iterateAll() as $city) {
  160.             /** @var City $city */
  161.             // Если включена фича вывода списка городов на главной странице, добавляем в sitemap для всех городов (в том числе и для дефолтного) страницу фильтра по городу;
  162.             // Если фича выключена, то для дефолтного города не добавляем страницу фильтра анкет по городу - она уже будет добавлена как роут "homepage".
  163.             if ($homepageAsCityList || !$city->equals($this->defaultCity)) {
  164.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_city', ['city' => $city->getUriIdentity()]) as $url) {
  165.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_city', 'geo');
  166.                 }
  167.             }
  168.             $this->registerCityLocationUrls($urlContainer, $city);
  169.             $this->registerCityStaticUrls($urlContainer, $city);
  170.             $this->registerCityNameUrls($urlContainer, $city);
  171.         }
  172.     }
  173.     protected function registerCityNameUrls(UrlContainerInterface $urlContainer, City $city): void
  174.     {
  175.         if (!$this->features->list_names()) {
  176.             return;
  177.         }
  178.         foreach ($this->generateLocalizedUrls('profile_list.list_names', ['city' => $city->getUriIdentity()]) as $url) {
  179.             $this->addListingUrl($urlContainer, $url, 'profile_list.list_names', 'profiles');
  180.         }
  181.         foreach ($this->profileNameLandingProvider->namesByCity($city) as $name) {
  182.             foreach ($this->generateLocalizedUrls('profile_list.list_by_name', [
  183.                 'city' => $city->getUriIdentity(),
  184.                 'name' => $name['uriIdentity'],
  185.             ]) as $url) {
  186.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_name', 'profiles');
  187.             }
  188.         }
  189.     }
  190.     protected function registerCityLocationUrls(UrlContainerInterface $urlContainer, City $city): void
  191.     {
  192.         foreach ($city->getCounties() as $county) {
  193.             foreach ($this->generateLocalizedUrls('profile_list.list_by_county', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  194.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county', 'geo');
  195.             }
  196.         }
  197.         foreach ($city->getDistricts() as $district) {
  198.             foreach ($this->generateLocalizedUrls('profile_list.list_by_district', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  199.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district', 'geo');
  200.             }
  201.         }
  202.         foreach ($city->getStations() as $station) {
  203.             foreach ($this->generateLocalizedUrls('profile_list.list_by_station', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  204.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station', 'geo');
  205.             }
  206.         }
  207.         if ($this->features->extra_category_eromassage()) {
  208.             foreach ($city->getCounties() as $county) {
  209.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_county_eromassage', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  210.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county_eromassage', 'categories');
  211.                 }
  212.             }
  213.             foreach ($city->getDistricts() as $district) {
  214.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_district_eromassage', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  215.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district_eromassage', 'categories');
  216.                 }
  217.             }
  218.             foreach ($city->getStations() as $station) {
  219.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_station_eromassage', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  220.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station_eromassage', 'categories');
  221.                 }
  222.             }
  223.         }
  224.         if ($this->features->extra_category_verified()) {
  225.             foreach ($city->getCounties() as $county) {
  226.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_county_verified', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  227.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county_verified', 'categories');
  228.                 }
  229.             }
  230.             foreach ($city->getDistricts() as $district) {
  231.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_district_verified', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  232.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district_verified', 'categories');
  233.                 }
  234.             }
  235.             foreach ($city->getStations() as $station) {
  236.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_station_verified', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  237.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station_verified', 'categories');
  238.                 }
  239.             }
  240.         }
  241.         if ($this->features->extra_category_cheap()) {
  242.             foreach ($city->getCounties() as $county) {
  243.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_county_cheap', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  244.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county_cheap', 'categories');
  245.                 }
  246.             }
  247.             foreach ($city->getDistricts() as $district) {
  248.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_district_cheap', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  249.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district_cheap', 'categories');
  250.                 }
  251.             }
  252.             foreach ($city->getStations() as $station) {
  253.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_station_cheap', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  254.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station_cheap', 'categories');
  255.                 }
  256.             }
  257.         }
  258.         if ($this->features->extra_category_mature()) {
  259.             foreach ($city->getCounties() as $county) {
  260.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_county_mature', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  261.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county_mature', 'categories');
  262.                 }
  263.             }
  264.             foreach ($city->getDistricts() as $district) {
  265.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_district_mature', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  266.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district_mature', 'categories');
  267.                 }
  268.             }
  269.             foreach ($city->getStations() as $station) {
  270.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_station_mature', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  271.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station_mature', 'categories');
  272.                 }
  273.             }
  274.         }
  275.         if ($this->features->extra_category_uzbek()) {
  276.             foreach ($city->getCounties() as $county) {
  277.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_county_uzbek', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  278.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_county_uzbek', 'categories');
  279.                 }
  280.             }
  281.             foreach ($city->getDistricts() as $district) {
  282.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_district_uzbek', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  283.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_district_uzbek', 'categories');
  284.                 }
  285.             }
  286.             foreach ($city->getStations() as $station) {
  287.                 foreach ($this->generateLocalizedUrls('profile_list.list_by_station_uzbek', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  288.                     $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_station_uzbek', 'categories');
  289.                 }
  290.             }
  291.         }
  292.         foreach ($this->generateLocalizedUrls('map.page', ['city' => $city->getUriIdentity()]) as $url) {
  293.             $this->addListingUrl($urlContainer, $url, 'map.page', 'geo');
  294.         }
  295.     }
  296.     protected function registerCityStaticUrls(UrlContainerInterface $urlContainer, City $city): void
  297.     {
  298.         if ($this->features->has_masseurs()) {
  299.             foreach ($this->generateLocalizedUrls('masseur_list.page', ['city' => $city->getUriIdentity()]) as $url) {
  300.                 $this->addListingUrl($urlContainer, $url, 'masseur_list.page', 'categories');
  301.             }
  302.         }
  303.         if ($this->features->has_saloons()) {
  304.             foreach ($this->generateLocalizedUrls('saloon_list.list_by_city', ['city' => $city->getUriIdentity()]) as $url) {
  305.                 $this->addListingUrl($urlContainer, $url, 'saloon_list.list_by_city', 'categories');
  306.             }
  307.         }
  308.         /*
  309.         if ($this->features->has_archive_page()) {
  310.             foreach ($this->generateLocalizedUrls('profile_list.list_archived', ['city' => $city->getUriIdentity()]) as $url) {
  311.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_archived', 'categories');
  312.             }
  313.         }
  314.         */
  315.         foreach ($this->serviceRepository->iterateAll() as $service) {
  316.             /** @var \App\Entity\Service $service */
  317.             foreach ($this->generateLocalizedUrls('profile_list.list_by_provided_service', ['city' => $city->getUriIdentity(), 'service' => $service->getUriIdentity()]) as $url) {
  318.                 $this->addListingUrl($urlContainer, $url, 'profile_list.list_by_provided_service', 'categories');
  319.             }
  320.         }
  321.         foreach ($this->profileInventoryItemRepository->iterateAll() as $item) {
  322.             if (!$this->isInventoryItemTypeEnabled($item)) {
  323.                 continue;
  324.             }
  325.             foreach ($this->generateLocalizedUrls('profile_list.list_by_inventory_item', ['city' => $city->getUriIdentity(), 'item' => $item->getUriIdentity()]) as $url) {
  326.                 $urlContainer->addUrl(new UrlConcrete(
  327.                     $url, $lastModified
  328.                 ), $this->getSitemapSectionName('categories'));
  329.             }
  330.         }
  331.         foreach ($this->findSitemapRoutesBySection('categories') as $route => $parameters) {
  332.             $parameters['city'] = $city->getUriIdentity();
  333.             foreach ($this->generateLocalizedUrls($route, $parameters) as $url) {
  334.                 $this->addListingUrl($urlContainer, $url, $route, 'categories');
  335.             }
  336.         }
  337.     }
  338.     private function isInventoryItemTypeEnabled(ProfileInventoryItem $item): bool
  339.     {
  340.         return match ($item->getType()) {
  341.             ProfileInventoryItemType::SEX_TOY => $this->features->sex_toys(),
  342.             ProfileInventoryItemType::CLOTHES_AND_UNDERWEAR => $this->features->clothes_and_underwear(),
  343.             ProfileInventoryItemType::ROLE_PLAYING_COSTUME => $this->features->role_playing_costumes(),
  344.             default => false,
  345.         };
  346.     }
  347.     protected function registerProfileUrls(UrlContainerInterface $urlContainer): void
  348.     {
  349.         foreach ($this->profileRepository->sitemapItemsIterator() as $profile) {
  350.             foreach ($this->generateLocalizedUrls('profile_preview.page', ['city' => $profile['city_uri'], 'profile' => $profile['uri']]) as $url) {
  351.                 $urlContainer->addUrl(new UrlConcrete(
  352.                     $url, $this->dateMutable($profile['updatedAt'])
  353.                 ), $this->getSitemapSectionName('profiles'));
  354.             }
  355.         }
  356.     }
  357.     protected function registerSaloonUrls(UrlContainerInterface $urlContainer): void
  358.     {
  359.         foreach ($this->saloonRepository->sitemapItemsIterator() as $saloon) {
  360.             foreach ($this->generateLocalizedUrls('saloon_preview.page', ['city' => $saloon['city_uri'], 'saloon' => $saloon['uri']]) as $url) {
  361.                 $urlContainer->addUrl(new UrlConcrete(
  362.                     $url, $this->dateMutable($saloon['updatedAt'])
  363.                 ), $this->getSitemapSectionName('saloons'));
  364.             }
  365.         }
  366.     }
  367.     /**
  368.      * Return overridden section name for sitemap file
  369.      */
  370.     protected function getSitemapSectionName(string $name): string
  371.     {
  372.         return $this->sitemapConfig['sections'][$name] ?? $name;
  373.     }
  374.     protected function findSitemapRoutesBySection(string $section): iterable
  375.     {
  376.         $processedRoutes = [];
  377.         foreach ($this->router->getRouteCollection() as $name => $route) {
  378.             if (true === $route->getDefault('_route_disabled')) {
  379.                 continue;
  380.             }
  381.             if (str_starts_with($name, DynamicRouter::DEFAULT_CITY_ROUTE_PREFIX)
  382.                 || str_starts_with($name, DynamicRouter::OVERRIDDEN_ROUTE_PREFIX)
  383.                 || str_ends_with($name, DynamicRouter::PAGINATION_ROUTE_POSTFIX)) {
  384.                 continue;
  385.             }
  386.             $config = $route->getOption(self::ROUTE_SITEMAP_OPTION);
  387.             if (empty($config) || $section !== ($config['section'] ?? null)) {
  388.                 continue;
  389.             }
  390.             if (null !== $this->features) {
  391.                 $routeFeature = $route->getDefault('_feature');
  392.                 if (null !== $routeFeature && !$this->features->isActive($routeFeature)) {
  393.                     continue;
  394.                 }
  395.             }
  396.             $canonical = $route->getDefault('_canonical_route');
  397.             if (null !== $canonical) {
  398.                 $name = $canonical;
  399.             }
  400.             if (array_key_exists($name, $processedRoutes)) {
  401.                 continue;
  402.             }
  403.             $processedRoutes[$name] = true;
  404.             $controller = $route->getDefault('_controller');
  405.             if (is_array($controller)) {
  406.                 $controller = "$controller[0]::$controller[1]";
  407.             }
  408.             if (null === $controller || !str_contains($controller, '::')) {
  409.                 continue;
  410.             }
  411.             [$class, ] = explode('::', $controller, 2);
  412.             if (!class_exists($class)) {
  413.                 continue;
  414.             }
  415.             if (!empty($config['data'])) {
  416.                 foreach ($this->resolveRouteEnumParameters($config['data']) as $parameters) {
  417.                     yield $name => $parameters;
  418.                 }
  419.             } else {
  420.                 yield $name => [];
  421.             }
  422.         }
  423.     }
  424.     private function prepareConfig(): void
  425.     {
  426.         $this->sitemapConfig = [];
  427.         if (!file_exists($this->sitemapConfigPath)) {
  428.             return;
  429.         }
  430.         try {
  431.             $this->sitemapConfig = Yaml::parseFile($this->sitemapConfigPath);
  432.         } catch (\Exception $e) {
  433.             trigger_error($e->getMessage(), E_USER_WARNING);
  434.         }
  435.     }
  436.     private function resolveRouteEnumParameters(array $data): iterable
  437.     {
  438.         $hasEnum = false;
  439.         $firstRow = $data[0];
  440.         foreach ($firstRow as $parameterName => $enumClass) {
  441.             if (is_string($enumClass) && in_array(EnumTrait::class, class_uses($enumClass), true)) {
  442.                 $hasEnum = true;
  443.                 foreach ($enumClass::getUriLocations() as $uri) {
  444.                     yield [$parameterName => $uri];
  445.                 }
  446.                 break;
  447.             }
  448.         }
  449.         if (!$hasEnum) {
  450.             return $data;
  451.         }
  452.     }
  453.     private function getSectionLastModified(string $section): \DateTime
  454.     {
  455.         // Дефолтные дни месяца для LastModified секций
  456.         $defaults = [
  457.             'geo' => 5,
  458.             'categories' => 20,
  459.         ];
  460.         if (!isset($defaults[$section])) {
  461.             throw new \InvalidArgumentException("Unknown section: $section");
  462.         }
  463.         $defaultLastModified = Carbon::create(null, null, $defaults[$section]);
  464.         if ($defaultLastModified->isFuture()) {
  465.             $defaultLastModified->subMonth();
  466.         }
  467.         $lastSwitch = $this->getLastDomainSwitch();
  468.         if (null !== $lastSwitch && $lastSwitch > $defaultLastModified) {
  469.             return $this->dateMutable($lastSwitch);
  470.         }
  471.         return $defaultLastModified;
  472.     }
  473.     private function getLastDomainSwitch(): ?\DateTimeImmutable
  474.     {
  475.         static $lastSwitch = null;
  476.         static $calledPreviously = false;
  477.         if (!$calledPreviously) {
  478.             try {
  479.                 $calledPreviously = true;
  480.                 $response = $this->httpClient->request('GET', '');
  481.                 $data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
  482.                 if (null === $data) { // empty timeline, no switches history
  483.                     $lastSwitch = null;
  484.                 } else {
  485.                     $lastSwitch = CarbonImmutable::parse($data['switchedAt']);
  486.                 }
  487.             } catch (\Exception $ex) {
  488.                 trigger_error('Failed to get last domain switch date. '.$ex->getMessage(), E_USER_WARNING);
  489.                 $lastSwitch = null;
  490.             }
  491.         }
  492.         return $lastSwitch;
  493.     }
  494. }