src/EventSubscriber/SitemapSubscriber.php line 86

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