src/EventSubscriber/SitemapSubscriber.php line 101

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