<?php
/**
* Created by simpson <simpsonwork@gmail.com>
* Date: 2019-04-18
* Time: 11:17
*/
namespace App\EventSubscriber;
use App\Entity\EnumTrait;
use App\Entity\Location\City;
use App\Entity\Profile\Profile;
use App\Entity\Service;
use App\Entity\Saloon\Saloon;
use App\Repository\CityRepository;
use App\Repository\ProfileRepository;
use App\Repository\SaloonRepository;
use App\Repository\ServiceRepository;
use App\Routing\DynamicRouter;
use App\Service\Features;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Doctrine\Persistence\ManagerRegistry;
use GuzzleHttp\ClientInterface;
use Presta\SitemapBundle\Event\SitemapPopulateEvent;
use Presta\SitemapBundle\Service\UrlContainerInterface;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Yaml\Yaml;
class SitemapSubscriber implements EventSubscriberInterface
{
public const ROUTE_SITEMAP_OPTION = 'sitemap.custom';
protected CityRepository $cityRepository;
protected ProfileRepository $profileRepository;
protected SaloonRepository $saloonRepository;
protected ServiceRepository $serviceRepository;
protected string $defaultCity;
protected ClientInterface $httpClient;
private array $sitemapConfig;
private ?array $routeLocales;
public function __construct(
protected RouterInterface $router,
private Features $features,
ManagerRegistry $registry,
ParameterBagInterface $parameterBag,
ClientInterface $apiDomainTimelineClient,
protected string $sitemapConfigPath,
)
{
$this->defaultCity = $parameterBag->get('default_city');
$this->cityRepository = $registry->getManagerForClass(City::class)->getRepository(City::class);
$this->profileRepository = $registry->getManagerForClass(Profile::class)->getRepository(Profile::class);
$this->saloonRepository = $registry->getManagerForClass(Saloon::class)->getRepository(Saloon::class);
$this->serviceRepository = $registry->getManagerForClass(Service::class)->getRepository(Service::class);
$this->httpClient = $apiDomainTimelineClient;
if ($this->features->has_translations()) {
$this->routeLocales = $this->features->sitemap_multiple_locales()
? ['ru', 'en']
: ['ru'];
} else {
$this->routeLocales = null;
}
}
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
SitemapPopulateEvent::ON_SITEMAP_POPULATE => 'populate',
];
}
public function populate(SitemapPopulateEvent $event): void
{
$this->prepareConfig();
$urlContainer = $event->getUrlContainer();
$this->registerHomepage($urlContainer);
$this->registerCityUrls($urlContainer);
$this->registerProfileUrls($urlContainer);
if ($this->features->has_saloons()) {
$this->registerSaloonUrls($urlContainer);
}
}
private function dateMutable(?\DateTimeImmutable $dateImmutable): ?\DateTime
{
if (null === $dateImmutable) {
return Carbon::now();
}
return Carbon::createFromTimestampUTC($dateImmutable->getTimestamp());
}
private function normalizeUriIdentity(string $value): string
{
$normalized = strtolower(str_replace('_', '-', $value));
return $normalized;
}
private function generateLocalizedUrls(string $canonicalRoute, array $routeParameters): iterable
{
if (null === $this->routeLocales) {
yield $this->router->generate($canonicalRoute, $routeParameters, UrlGeneratorInterface::ABSOLUTE_URL);
} else {
foreach ($this->routeLocales as $routeLocale) {
yield $this->router->generate("$canonicalRoute.$routeLocale", $routeParameters, UrlGeneratorInterface::ABSOLUTE_URL);
}
}
}
protected function registerHomepage(UrlContainerInterface $urlContainer): void
{
$lastModified = Carbon::now();
foreach ($this->generateLocalizedUrls('homepage', []) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
protected function registerCityUrls(UrlContainerInterface $urlContainer): void
{
$lastModified = $this->getSectionLastModified('geo');
$homepageAsCityList = $this->features->homepage_as_city_list();
foreach ($this->cityRepository->iterateAll() as $city) {
/** @var City $city */
// Если включена фича вывода списка городов на главной странице, добавляем в sitemap для всех городов (в том числе и для дефолтного) страницу фильтра по городу;
// Если фича выключена, то для дефолтного города не добавляем страницу фильтра анкет по городу - она уже будет добавлена как роут "homepage".
if ($homepageAsCityList || !$city->equals($this->defaultCity)) {
foreach ($this->generateLocalizedUrls('profile_list.list_by_city', ['city' => $city->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
$this->registerCityLocationUrls($urlContainer, $city);
$this->registerCityStaticUrls($urlContainer, $city);
}
}
protected function registerCityLocationUrls(UrlContainerInterface $urlContainer, City $city): void
{
$lastModified = $this->getSectionLastModified('geo');
foreach ($city->getCounties() as $county) {
foreach ($this->generateLocalizedUrls('profile_list.list_by_county', ['city' => $city->getUriIdentity(), 'county' => $county->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
foreach ($city->getDistricts() as $district) {
foreach ($this->generateLocalizedUrls('profile_list.list_by_district', ['city' => $city->getUriIdentity(), 'district' => $district->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
foreach ($city->getStations() as $station) {
foreach ($this->generateLocalizedUrls('profile_list.list_by_station', ['city' => $city->getUriIdentity(), 'station' => $station->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
foreach ($this->generateLocalizedUrls('map.page', ['city' => $city->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('geo'));
}
}
protected function registerCityStaticUrls(UrlContainerInterface $urlContainer, City $city): void
{
$lastModified = $this->getSectionLastModified('categories');
if ($this->features->has_masseurs()) {
foreach ($this->generateLocalizedUrls('masseur_list.page', ['city' => $city->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('categories'));
}
}
if ($this->features->has_saloons()) {
foreach ($this->generateLocalizedUrls('saloon_list.list_by_city', ['city' => $city->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('categories'));
}
}
/*
if ($this->features->has_archive_page()) {
foreach ($this->generateLocalizedUrls('profile_list.list_archived', ['city' => $city->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('categories'));
}
}
*/
foreach ($this->serviceRepository->iterateAll() as $service) {
/** @var \App\Entity\Service $service */
foreach ($this->generateLocalizedUrls('profile_list.list_by_provided_service', ['city' => $city->getUriIdentity(), 'service' => $service->getUriIdentity()]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('categories'));
}
}
foreach ($this->findSitemapRoutesBySection('categories') as $route => $parameters) {
$parameters['city'] = $city->getUriIdentity();
foreach ($this->generateLocalizedUrls($route, $parameters) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $lastModified
), $this->getSitemapSectionName('categories'));
}
}
}
protected function registerProfileUrls(UrlContainerInterface $urlContainer): void
{
foreach ($this->profileRepository->sitemapItemsIterator() as $profile) {
foreach ($this->generateLocalizedUrls('profile_preview.page', ['city' => $profile['city_uri'], 'profile' => $profile['uri']]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $this->dateMutable($profile['updatedAt'])
), $this->getSitemapSectionName('profiles'));
}
}
}
protected function registerSaloonUrls(UrlContainerInterface $urlContainer): void
{
foreach ($this->saloonRepository->sitemapItemsIterator() as $saloon) {
foreach ($this->generateLocalizedUrls('saloon_preview.page', ['city' => $saloon['city_uri'], 'saloon' => $saloon['uri']]) as $url) {
$urlContainer->addUrl(new UrlConcrete(
$url, $this->dateMutable($saloon['updatedAt'])
), $this->getSitemapSectionName('saloons'));
}
}
}
/**
* Return overridden section name for sitemap file
*/
protected function getSitemapSectionName(string $name): string
{
return $this->sitemapConfig['sections'][$name] ?? $name;
}
protected function findSitemapRoutesBySection(string $section): iterable
{
$processedRoutes = [];
foreach ($this->router->getRouteCollection() as $name => $route) {
if (true === $route->getDefault('_route_disabled')) {
continue;
}
if (str_starts_with($name, DynamicRouter::DEFAULT_CITY_ROUTE_PREFIX)
|| str_starts_with($name, DynamicRouter::OVERRIDDEN_ROUTE_PREFIX)
|| str_ends_with($name, DynamicRouter::PAGINATION_ROUTE_POSTFIX)) {
continue;
}
$config = $route->getOption(self::ROUTE_SITEMAP_OPTION);
if (empty($config) || $section !== ($config['section'] ?? null)) {
continue;
}
if (null !== $this->features) {
$routeFeature = $route->getDefault('_feature');
if (null !== $routeFeature && !$this->features->isActive($routeFeature)) {
continue;
}
}
$canonical = $route->getDefault('_canonical_route');
if (null !== $canonical) {
$name = $canonical;
}
if (array_key_exists($name, $processedRoutes)) {
continue;
}
$processedRoutes[$name] = true;
$controller = $route->getDefault('_controller');
if (null === $controller || !str_contains($controller, '::')) {
continue;
}
[$class, ] = explode('::', $controller, 2);
if (!class_exists($class)) {
continue;
}
if (!empty($config['data'])) {
foreach ($this->resolveRouteEnumParameters($config['data']) as $parameters) {
yield $name => $parameters;
}
} else {
yield $name => [];
}
}
}
private function prepareConfig(): void
{
$this->sitemapConfig = [];
if (!file_exists($this->sitemapConfigPath)) {
return;
}
try {
$this->sitemapConfig = Yaml::parseFile($this->sitemapConfigPath);
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
}
}
private function resolveRouteEnumParameters(array $data): iterable
{
$hasEnum = false;
$firstRow = $data[0];
foreach ($firstRow as $parameterName => $enumClass) {
if (is_string($enumClass) && in_array(EnumTrait::class, class_uses($enumClass), true)) {
$hasEnum = true;
foreach ($enumClass::getUriLocations() as $uri) {
yield [$parameterName => $uri];
}
break;
}
}
if (!$hasEnum) {
return $data;
}
}
private function getSectionLastModified(string $section): \DateTime
{
// Дефолтные дни месяца для LastModified секций
$defaults = [
'geo' => 5,
'categories' => 20,
];
if (!isset($defaults[$section])) {
throw new \InvalidArgumentException("Unknown section: $section");
}
$defaultLastModified = Carbon::create(null, null, $defaults[$section]);
if ($defaultLastModified->isFuture()) {
$defaultLastModified->subMonth();
}
$lastSwitch = $this->getLastDomainSwitch();
if (null !== $lastSwitch && $lastSwitch > $defaultLastModified) {
return $this->dateMutable($lastSwitch);
}
return $defaultLastModified;
}
private function getLastDomainSwitch(): ?\DateTimeImmutable
{
static $lastSwitch = null;
static $calledPreviously = false;
if (!$calledPreviously) {
try {
$calledPreviously = true;
$response = $this->httpClient->request('GET', '');
$data = json_decode($response->getBody()->getContents(), true);
$lastSwitch = CarbonImmutable::parse($data['switchedAt']);
} catch (\Exception $ex) {
trigger_error('Failed to get last domain switch date. '.$ex->getMessage(), E_USER_WARNING);
$lastSwitch = null;
}
}
return $lastSwitch;
}
}