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.         foreach ($city->getCounties() as $county) {
  139.             foreach ($this->generateLocalizedUrls('profile_list.list_by_county', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
  140.                 $urlContainer->addUrl(new UrlConcrete(
  141.                     $url$lastModified
  142.                 ), $this->getSitemapSectionName('geo'));
  143.             }
  144.         }
  145.         foreach ($city->getDistricts() as $district) {
  146.             foreach ($this->generateLocalizedUrls('profile_list.list_by_district', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
  147.                 $urlContainer->addUrl(new UrlConcrete(
  148.                     $url$lastModified
  149.                 ), $this->getSitemapSectionName('geo'));
  150.             }
  151.         }
  152.         foreach ($city->getStations() as $station) {
  153.             foreach ($this->generateLocalizedUrls('profile_list.list_by_station', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
  154.                 $urlContainer->addUrl(new UrlConcrete(
  155.                     $url$lastModified
  156.                 ), $this->getSitemapSectionName('geo'));
  157.             }
  158.         }
  159.         foreach ($this->generateLocalizedUrls('map.page', ['city' => $city->getUriIdentity()]) as $url) {
  160.             $urlContainer->addUrl(new UrlConcrete(
  161.                 $url$lastModified
  162.             ), $this->getSitemapSectionName('geo'));
  163.         }
  164.     }
  165.     protected function registerCityStaticUrls(UrlContainerInterface $urlContainerCity $city): void
  166.     {
  167.         $lastModified $this->getSectionLastModified('categories');
  168.         if ($this->features->has_masseurs()) {
  169.             foreach ($this->generateLocalizedUrls('masseur_list.page', ['city' => $city->getUriIdentity()]) as $url) {
  170.                 $urlContainer->addUrl(new UrlConcrete(
  171.                     $url$lastModified
  172.                 ), $this->getSitemapSectionName('categories'));
  173.             }
  174.         }
  175.         if ($this->features->has_saloons()) {
  176.             foreach ($this->generateLocalizedUrls('saloon_list.list_by_city', ['city' => $city->getUriIdentity()]) as $url) {
  177.                 $urlContainer->addUrl(new UrlConcrete(
  178.                     $url$lastModified
  179.                 ), $this->getSitemapSectionName('categories'));
  180.             }
  181.         }
  182.         /*
  183.         if ($this->features->has_archive_page()) {
  184.             foreach ($this->generateLocalizedUrls('profile_list.list_archived', ['city' => $city->getUriIdentity()]) as $url) {
  185.                 $urlContainer->addUrl(new UrlConcrete(
  186.                     $url, $lastModified
  187.                 ), $this->getSitemapSectionName('categories'));
  188.             }
  189.         }
  190.         */
  191.         foreach ($this->serviceRepository->iterateAll() as $service) {
  192.             /** @var \App\Entity\Service $service */
  193.             foreach ($this->generateLocalizedUrls('profile_list.list_by_provided_service', ['city' => $city->getUriIdentity(), 'service' => $service->getUriIdentity()]) as $url) {
  194.                 $urlContainer->addUrl(new UrlConcrete(
  195.                     $url$lastModified
  196.                 ), $this->getSitemapSectionName('categories'));
  197.             }
  198.         }
  199.         foreach ($this->findSitemapRoutesBySection('categories') as $route => $parameters) {
  200.             $parameters['city'] = $city->getUriIdentity();
  201.             foreach ($this->generateLocalizedUrls($route$parameters) as $url) {
  202.                 $urlContainer->addUrl(new UrlConcrete(
  203.                     $url$lastModified
  204.                 ), $this->getSitemapSectionName('categories'));
  205.             }
  206.         }
  207.     }
  208.     protected function registerProfileUrls(UrlContainerInterface $urlContainer): void
  209.     {
  210.         foreach ($this->profileRepository->sitemapItemsIterator() as $profile) {
  211.             foreach ($this->generateLocalizedUrls('profile_preview.page', ['city' => $profile['city_uri'], 'profile' => $profile['uri']]) as $url) {
  212.                 $urlContainer->addUrl(new UrlConcrete(
  213.                     $url$this->dateMutable($profile['updatedAt'])
  214.                 ), $this->getSitemapSectionName('profiles'));
  215.             }
  216.         }
  217.     }
  218.     protected function registerSaloonUrls(UrlContainerInterface $urlContainer): void
  219.     {
  220.         foreach ($this->saloonRepository->sitemapItemsIterator() as $saloon) {
  221.             foreach ($this->generateLocalizedUrls('saloon_preview.page', ['city' => $saloon['city_uri'], 'saloon' => $saloon['uri']]) as $url) {
  222.                 $urlContainer->addUrl(new UrlConcrete(
  223.                     $url$this->dateMutable($saloon['updatedAt'])
  224.                 ), $this->getSitemapSectionName('saloons'));
  225.             }
  226.         }
  227.     }
  228.     /**
  229.      * Return overridden section name for sitemap file
  230.      */
  231.     protected function getSitemapSectionName(string $name): string
  232.     {
  233.         return $this->sitemapConfig['sections'][$name] ?? $name;
  234.     }
  235.     protected function findSitemapRoutesBySection(string $section): iterable
  236.     {
  237.         $processedRoutes = [];
  238.         foreach ($this->router->getRouteCollection() as $name => $route) {
  239.             if (true === $route->getDefault('_route_disabled')) {
  240.                 continue;
  241.             }
  242.             if (str_starts_with($nameDynamicRouter::DEFAULT_CITY_ROUTE_PREFIX)
  243.                 || str_starts_with($nameDynamicRouter::OVERRIDDEN_ROUTE_PREFIX)
  244.                 || str_ends_with($nameDynamicRouter::PAGINATION_ROUTE_POSTFIX)) {
  245.                 continue;
  246.             }
  247.             $config $route->getOption(self::ROUTE_SITEMAP_OPTION);
  248.             if (empty($config) || $section !== ($config['section'] ?? null)) {
  249.                 continue;
  250.             }
  251.             if (null !== $this->features) {
  252.                 $routeFeature $route->getDefault('_feature');
  253.                 if (null !== $routeFeature && !$this->features->isActive($routeFeature)) {
  254.                     continue;
  255.                 }
  256.             }
  257.             $canonical $route->getDefault('_canonical_route');
  258.             if (null !== $canonical) {
  259.                 $name $canonical;
  260.             }
  261.             if (array_key_exists($name$processedRoutes)) {
  262.                 continue;
  263.             }
  264.             $processedRoutes[$name] = true;
  265.             $controller $route->getDefault('_controller');
  266.             if (null === $controller || !str_contains($controller'::')) {
  267.                 continue;
  268.             }
  269.             [$class, ] = explode('::'$controller2);
  270.             if (!class_exists($class)) {
  271.                 continue;
  272.             }
  273.             if (!empty($config['data'])) {
  274.                 foreach ($this->resolveRouteEnumParameters($config['data']) as $parameters) {
  275.                     yield $name => $parameters;
  276.                 }
  277.             } else {
  278.                 yield $name => [];
  279.             }
  280.         }
  281.     }
  282.     private function prepareConfig(): void
  283.     {
  284.         $this->sitemapConfig = [];
  285.         if (!file_exists($this->sitemapConfigPath)) {
  286.             return;
  287.         }
  288.         try {
  289.             $this->sitemapConfig Yaml::parseFile($this->sitemapConfigPath);
  290.         } catch (\Exception $e) {
  291.             trigger_error($e->getMessage(), E_USER_WARNING);
  292.         }
  293.     }
  294.     private function resolveRouteEnumParameters(array $data): iterable
  295.     {
  296.         $hasEnum false;
  297.         $firstRow $data[0];
  298.         foreach ($firstRow as $parameterName => $enumClass) {
  299.             if (is_string($enumClass) && in_array(EnumTrait::class, class_uses($enumClass), true)) {
  300.                 $hasEnum true;
  301.                 foreach ($enumClass::getUriLocations() as $uri) {
  302.                     yield [$parameterName => $uri];
  303.                 }
  304.                 break;
  305.             }
  306.         }
  307.         if (!$hasEnum) {
  308.             return $data;
  309.         }
  310.     }
  311.     private function getSectionLastModified(string $section): \DateTime
  312.     {
  313.         // Дефолтные дни месяца для LastModified секций
  314.         $defaults = [
  315.             'geo' => 5,
  316.             'categories' => 20,
  317.         ];
  318.         if (!isset($defaults[$section])) {
  319.             throw new \InvalidArgumentException("Unknown section: $section");
  320.         }
  321.         $defaultLastModified Carbon::create(nullnull$defaults[$section]);
  322.         if ($defaultLastModified->isFuture()) {
  323.             $defaultLastModified->subMonth();
  324.         }
  325.         $lastSwitch $this->getLastDomainSwitch();
  326.         if (null !== $lastSwitch && $lastSwitch $defaultLastModified) {
  327.             return $this->dateMutable($lastSwitch);
  328.         }
  329.         return $defaultLastModified;
  330.     }
  331.     private function getLastDomainSwitch(): ?\DateTimeImmutable
  332.     {
  333.         static $lastSwitch null;
  334.         static $calledPreviously false;
  335.         if (!$calledPreviously) {
  336.             try {
  337.                 $calledPreviously true;
  338.                 $response $this->httpClient->request('GET''');
  339.                 $data json_decode($response->getBody()->getContents(), true);
  340.                 $lastSwitch CarbonImmutable::parse($data['switchedAt']);
  341.             } catch (\Exception $ex) {
  342.                 trigger_error('Failed to get last domain switch date. '.$ex->getMessage(), E_USER_WARNING);
  343.                 $lastSwitch null;
  344.             }
  345.         }
  346.         return $lastSwitch;
  347.     }
  348. }