src/EventSubscriber/SitemapSubscriber.php line 96

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