src/EventSubscriber/SitemapSubscriber.php line 87

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