src/EventSubscriber/SitemapSubscriber.php line 109

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