<?php
declare(strict_types=1);
namespace App\Service;
use App\Bridge\Porpaginas\Doctrine\ORM\FakeORMQueryPage;
use App\Entity\Location\City;
use App\Entity\Profile\Genders;
use App\Entity\Profile\Profile;
use App\Repository\ReadModel\ProfileListingReadModel;
use App\Specification\ElasticSearch\ISpecification;
use Porpaginas\Page;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class ProfileListingRecommendationsFiller
{
private int $perPage;
public function __construct(
private ProfileRecommendationService $profileRecommendationService,
private ProfileList $profileList,
private LoggerInterface $logger,
ParameterBagInterface $parameterBag,
) {
$this->perPage = (int)$parameterBag->get('profile_list.per_page');
}
public function fill(City $city, Page $profiles, ?ISpecification $recommendationSpec, array $genders = [Genders::FEMALE]): array
{
if (!$this->shouldFill($profiles)) {
return [
'profiles' => $profiles,
'applied' => false,
'original_count' => $profiles->totalCount(),
];
}
$currentProfiles = iterator_to_array($profiles->getIterator());
$excludeProfileIds = $this->extractProfileIds($currentProfiles);
$neededCount = $this->perPage - count($currentProfiles);
$recommendedListingProfiles = $this->loadRecommendedListingProfiles($city, $recommendationSpec, $genders, $excludeProfileIds, $neededCount);
if (empty($recommendedListingProfiles)) {
return [
'profiles' => $profiles,
'applied' => false,
'original_count' => $profiles->totalCount(),
];
}
$filledProfiles = array_merge($currentProfiles, $recommendedListingProfiles);
$originalCount = $profiles->totalCount();
return [
'profiles' => new FakeORMQueryPage(
$profiles->getCurrentOffset(),
$profiles->getCurrentPage(),
$profiles->getCurrentLimit(),
$originalCount + count($recommendedListingProfiles),
$filledProfiles,
),
'applied' => true,
'original_count' => $originalCount,
];
}
public function recommendedProfilesForPage(City $city, Page $items, ?ISpecification $recommendationSpec, array $genders = [Genders::FEMALE]): array
{
if (!$this->shouldFill($items)) {
return [
'profiles' => [],
'applied' => false,
'original_count' => $items->totalCount(),
];
}
$recommendedListingProfiles = $this->loadRecommendedListingProfiles(
$city,
$recommendationSpec,
$genders,
[],
$this->perPage - $items->count(),
);
return [
'profiles' => $recommendedListingProfiles,
'applied' => !empty($recommendedListingProfiles),
'original_count' => $items->totalCount(),
];
}
private function shouldFill(Page $profiles): bool
{
if ($profiles->count() >= $this->perPage) {
return false;
}
if (0 === $profiles->totalCount()) {
return 1 === $profiles->getCurrentPage();
}
return $profiles->getCurrentOffset() < $profiles->totalCount();
}
private function extractProfileIds(array $profiles): array
{
$ids = [];
foreach ($profiles as $profile) {
if ($profile instanceof Profile) {
$ids[] = $profile->getId();
continue;
}
if (isset($profile->id) && is_numeric($profile->id)) {
$ids[] = (int)$profile->id;
}
}
return array_values(array_unique($ids));
}
private function resolveGender(array $genders): int
{
$gender = reset($genders);
return is_numeric($gender) ? (int)$gender : Genders::FEMALE;
}
private function loadRecommendedListingProfiles(
City $city,
?ISpecification $recommendationSpec,
array $genders,
array $excludeProfileIds,
int $neededCount,
): array {
if ($neededCount <= 0) {
return [];
}
try {
$recommendedProfiles = $this->profileRecommendationService->getRecommendations(
$city,
$recommendationSpec?->getSpec()->toEsQueryObject(),
[],
[],
$excludeProfileIds,
$neededCount,
$this->resolveGender($genders),
);
} catch (\Throwable $exception) {
$this->logger->warning('Failed to fill listing with profile recommendations.', [
'city_id' => $city->getId(),
'exception' => $exception,
]);
return [];
}
$recommendedProfileIds = array_map(
static fn(Profile $profile): int => $profile->getId(),
$recommendedProfiles,
);
return $this->profileList->getProfilesByIdsInOrder($recommendedProfileIds);
}
}