<?php
namespace App\Service;
use App\Dto\HomepageCityListingsBlockData;
use App\Entity\Location\City;
use App\Repository\SaloonRepository;
use App\Specification\QueryModifier\FreeProfilesFeatureSaloonOrder;
use App\Specification\QueryModifier\SaloonOrderedByStatus;
use App\Specification\Saloon\SaloonIsActive;
use App\Specification\Saloon\SaloonIsLocated;
use App\Specification\Saloon\SaloonIsNotHidden;
use Happyr\DoctrineSpecification\Spec;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class HomepageCityListingsBlockProvider
{
private const CACHE_KEY_MASSEURS_FALLBACK_ITEMS = 'homepage_city_masseurs_fallback_items';
private const CACHE_KEY_SALOONS_IDS = 'homepage_city_saloons_ids';
public function __construct(
private ListingRotationApi $listingRotationApi,
private ProfileList $profileList,
private ProfileListSpecificationService $profileListSpecificationService,
private SaloonRepository $saloonRepository,
private Features $features,
private ParameterBagInterface $parameterBag,
private CacheItemPoolInterface $projectDataCache,
) {}
public function getForCity(City $city): HomepageCityListingsBlockData
{
$masseurs = [];
$saloons = [];
if ($this->features->has_masseurs() && $this->features->homepage_city_masseurs_block()) {
$masseurs = $this->getMasseursFromListing($city, $this->getMasseursCount());
}
if ($this->features->has_saloons() && $this->features->homepage_city_saloons_block()) {
$saloons = $this->getSaloonsFromListing($city, $this->getSaloonsCount());
}
return new HomepageCityListingsBlockData($masseurs, $saloons);
}
private function getMasseursFromListing(City $city, int $count): array
{
if ($count < 1) {
return [];
}
try {
return $this->listingRotationApi->listLimited('/city/{city}/masseur', ['city' => $city->getId()], $count);
} catch (\Throwable) {
try {
$specs = $this->profileListSpecificationService->listForMasseur($city);
$cacheKey = sprintf('%s_%s_%s', self::CACHE_KEY_MASSEURS_FALLBACK_ITEMS, $city->getId(), $count);
return $this->rememberItems($cacheKey, function() use ($city, $specs, $count): array {
$result = $this->profileList->listActiveWithinCityOrderedByStatusWithSpecLimited(
$city,
$specs->spec(),
$specs->additionalSpecs(),
$specs->genders(),
true,
$count,
);
return is_array($result) ? $result : [];
});
} catch (\Throwable) {
return [];
}
}
}
private function getSaloonsFromListing(City $city, int $count): array
{
if ($count < 1) {
return [];
}
$cacheKey = sprintf(
'%s_%s_%s_%s',
self::CACHE_KEY_SALOONS_IDS,
$city->getId(),
$count,
(int) $this->features->free_profiles(),
);
$ids = $this->rememberIds($cacheKey, function() use ($city, $count): array {
$criteria = Spec::andX(
$this->features->free_profiles() ? new SaloonIsNotHidden() : new SaloonIsActive(),
SaloonIsLocated::withinCity($city),
$this->features->free_profiles() ? new FreeProfilesFeatureSaloonOrder() : new SaloonOrderedByStatus()
);
$result = $this->saloonRepository->matchingSpec($criteria)->take(0, $count);
return $this->extractIds($result, $count);
});
if (empty($ids)) {
return [];
}
$saloons = $this->saloonRepository->findBy(['id' => $ids]);
return $this->sortByIds($saloons, $ids);
}
private function rememberIds(string $key, callable $resolver): array
{
$cacheItem = $this->projectDataCache->getItem($key);
if ($cacheItem->isHit()) {
$value = $cacheItem->get();
return is_array($value) ? $value : [];
}
$value = $resolver();
$ids = array_values(array_unique(array_filter($value, static function($id): bool {
return is_int($id) || ctype_digit((string) $id);
})));
$cacheItem->set($ids);
$cacheItem->expiresAfter(60);
$this->projectDataCache->save($cacheItem);
return $ids;
}
private function rememberItems(string $key, callable $resolver): array
{
$cacheItem = $this->projectDataCache->getItem($key);
if ($cacheItem->isHit()) {
$value = $cacheItem->get();
return is_array($value) ? $value : [];
}
$value = $resolver();
$items = is_array($value) ? $value : [];
$cacheItem->set($items);
$cacheItem->expiresAfter(60);
$this->projectDataCache->save($cacheItem);
return $items;
}
private function extractIds(iterable $items, int $limit): array
{
$ids = [];
foreach ($items as $item) {
$id = $this->extractId($item);
if (null === $id) {
continue;
}
$ids[] = $id;
if (count($ids) >= $limit) {
break;
}
}
return $ids;
}
private function extractId(mixed $item): ?int
{
if (is_object($item) && method_exists($item, 'getId')) {
return (int) $item->getId();
}
if (is_object($item) && isset($item->id)) {
return (int) $item->id;
}
if (is_array($item) && isset($item['id'])) {
return (int) $item['id'];
}
return null;
}
private function sortByIds(array $items, array $ids): array
{
$itemsById = [];
foreach ($items as $item) {
$id = $this->extractId($item);
if (null !== $id) {
$itemsById[$id] = $item;
}
}
$sorted = [];
foreach ($ids as $id) {
if (isset($itemsById[$id])) {
$sorted[] = $itemsById[$id];
}
}
return $sorted;
}
private function getMasseursCount(): int
{
return max(0, (int) $this->parameterBag->get('homepage.city.masseurs_block.count'));
}
private function getSaloonsCount(): int
{
return max(0, (int) $this->parameterBag->get('homepage.city.saloons_block.count'));
}
}