<?php
namespace App\Service;
use App\Bridge\Porpaginas\Doctrine\ORM\FakeORMQueryPage;
use App\Entity\Location\City;
use App\Entity\Profile\Genders;
use App\Event\PaginatorPageTakenEvent;
use App\Repository\ProfileRepository;
use App\Repository\ReadModel\ProfileListingReadModel;
use App\Specification\Profile\ICountIdSpec;
use App\Specification\Profile\ISelectIdListSpec;
use App\Specification\Profile\ProfileHasOneOfGenders;
use App\Specification\Profile\ProfileIdINOrderedByINValues;
use App\Specification\Profile\ProfileIdNotIn;
use App\Specification\Profile\ProfileIsActive;
use App\Specification\Profile\ProfileIsArchived;
use App\Specification\Profile\ProfileIsLocatedInCountry;
use App\Specification\Profile\ProfileIsLocated;
use App\Specification\Profile\ProfileIsMasseur;
use App\Specification\Profile\ProfileIsModerationPassed;
use App\Specification\Profile\ProfileIsNotMasseur;
use App\Specification\Profile\ProfileIsNotRejected;
use App\Specification\QueryModifier\FreeProfilesFeatureArchivedProfileOrder;
use App\Specification\QueryModifier\FreeProfilesFeatureProfileOrder;
use App\Specification\Profile\ProfileIsHidden;
use App\Specification\Profile\ProfileIsNotHidden;
use App\Specification\QueryModifier\LimitResult;
use App\Specification\QueryModifier\ProfileOrderedByCreated;
use App\Specification\QueryModifier\ProfileOrderedByInactivated;
use App\Specification\QueryModifier\ProfileOrderedByRandom;
use App\Specification\QueryModifier\ProfileOrderedByUpdated;
use App\Specification\QueryModifier\ProfileOrderedByStatus;
use App\Specification\QueryModifier\ProfilePlacementHiding;
use Doctrine\ORM\EntityManagerInterface;
use Happyr\DoctrineSpecification\Filter\Filter;
use Happyr\DoctrineSpecification\Logic\AndX;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\Specification;
use Porpaginas\Page;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ProfileList
{
public const ORDER_BY_STATUS = 'status';
public const ORDER_BY_UPDATED = 'updated';
public const ORDER_NONE = 'none';
private ?Request $request;
private int $perPageDefault;
private int $perPage;
private array $executedCountQueryData;
public function __construct(
private ProfileRepository $profileRepository,
RequestStack $requestStack,
private Features $features,
ParameterBagInterface $parameterBag,
private EventDispatcherInterface $eventDispatcher,
private EntityManagerInterface $entityManager,
private ProfileTopBoard $profileTopBoard
)
{
$this->request = $requestStack->getCurrentRequest();
$this->perPageDefault = intval($parameterBag->get('profile_list.per_page'));
$this->perPage = $this->perPageDefault;
}
public function listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacement(
City $city, ?Filter $spec, array $additionalSpecs = null, array $genders = [Genders::FEMALE]
): array|Page
{
return $this->listActiveWithinCityOrderedByStatusWithSpec($city, $spec, $additionalSpecs, $genders, true);
}
public function listActiveWithinCityOrderedByStatusWithSpec(
City $city, ?Filter $spec, array $additionalSpecs = null, array $genders = [Genders::FEMALE], bool $avoidTopPlacement = false
): array|Page
{
return $this->list($city, null, $spec, $additionalSpecs, true, null, self::ORDER_BY_STATUS,
null, true, null, $genders, $avoidTopPlacement);
}
public function listActiveWithinCityOrderedByStatusWithSpecLimited(
City $city, ?Filter $spec, array $additionalSpecs = null, array $genders = [Genders::FEMALE], bool $avoidTopPlacement = false, int $limit = 0,
): array|Page
{
return $this->list($city, null, $spec, $additionalSpecs, true, null, self::ORDER_BY_STATUS,
$limit, false, null, $genders, $avoidTopPlacement);
}
public function listActiveNotMasseurWithinCityOrderedByStatusWithSpec(
City $city, ?Filter $spec, array $additionalSpecs = null, array $genders = [Genders::FEMALE]
): array|Page
{
return $this->list($city, null, $spec, $additionalSpecs, true, false, self::ORDER_BY_STATUS,
null, true, null, $genders);
}
public function list(
City $city, ?string $country, ?Filter $spec, ?array $additionalSpecs, bool $active, ?bool $masseur = false,
?string $order = self::ORDER_BY_STATUS, ?int $limit = null, bool $paged = true, ?callable $fetchByIdMethod = null,
array $genders = [Genders::FEMALE], bool $avoidTopPlacement = false
): array|Page
{
// $this->perPage = $limit ?? $this->perPageDefault;
$this->perPage = $this->perPageDefault;
$topPlacementToAvoidId = null;
if(/*null === $limit && */true === $avoidTopPlacement) {
$profileTopPlacement = $this->profileTopBoard->currentTopPlacement(false);
if(null !== $profileTopPlacement) {
$topPlacementToAvoidId = $profileTopPlacement->getId();
//на тесте часто ставят 1 на страницу, что ломает логику, т.к. после декремента становится 0 на страницу,
//за тем и условие
if($this->perPage > 1) {
$this->perPage--;
}
}
}
$order = $this->getOrderSpecByFlags($order, $active);
$masseurSpec = $this->getMasseurSpecByFlag($masseur);
$activeSpec = $this->getActiveSpecByFlag($active);
$excludeTopProfileSpec = null !== $topPlacementToAvoidId ? new ProfileIdNotIn([$topPlacementToAvoidId]) : null;
$criteria = Spec::andX(
$country ? ProfileIsLocatedInCountry::withCountryCode($city->getCountryCode()) : ProfileIsLocated::withinCity($city),
$activeSpec
);
if($masseurSpec)
$criteria->andX($masseurSpec);
if(null !== $excludeTopProfileSpec)
$criteria->andX($excludeTopProfileSpec);
$criteria->andX($this->getModerationSpecByFlag());
$criteria->andX(new ProfileHasOneOfGenders($genders));
$criteriaForIdList = clone $criteria;
if($paged) {
$count = $this->countOfCriteriaWithCustomSpec($criteria, $spec, $additionalSpecs);
if(0 == $count)
return $this->takeFakePage($count, []);
}
if($order)
$criteriaForIdList->andX($order);
$idList = $this->listIdOfCriteriaWithCustomSpec($criteriaForIdList, $spec, $additionalSpecs, $paged, $limit);
$profiles = [];
if(!empty($idList)) {
// $profiles = $this->profileRepository->matchingSpecRaw(new ProfileIdINOrderedByINValues($idList), null, true, array($this->profileRepository, 'hydrateProfileRow'));
$profiles = null == $fetchByIdMethod
? $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList))
: $fetchByIdMethod(new ProfileIdINOrderedByINValues($idList));
}
if($paged) {
$profiles = $this->takeFakePage($count, $profiles);
}
$this->restorePerPageToDefault();
return $profiles;
}
protected function countOfCriteriaWithCustomSpec(AndX $criteria, ?Filter $spec, array $additionalSpecs = null): int
{
if(null == $additionalSpecs)
$additionalSpecs = [];
array_unshift($additionalSpecs, $spec);
array_walk($additionalSpecs, function($spec) use ($criteria): void {
$criteria->andX($spec);
if($spec instanceof ICountIdSpec)
$criteria->andX($spec->getCountIdSpec());
});
$defaultStack = $this->entityManager->getConnection()->getConfiguration()->getSQLLogger();
$stack = new \Doctrine\DBAL\Logging\DebugStack();
$this->entityManager->getConnection()->getConfiguration()->setSQLLogger($stack);
$result = $this->profileRepository->countMatchingSpec($criteria);
//$this->executedCountQueryData = ['sql' => 'SELECT count( p0_.id ) AS sclr_0 FROM `profiles` p0_ INNER JOIN profile_adboard_placements p1_ ON p0_.id = p1_.profile_id WHERE ( p0_.deleted_at IS NULL )', 'params' => [], 'types' => []];//
$this->executedCountQueryData = $stack->queries[count($stack->queries)];
$this->entityManager->getConnection()->getConfiguration()->setSQLLogger($defaultStack);
return $result;
}
protected function listIdOfCriteriaWithCustomSpec(AndX $criteria, ?Filter $spec, array $additionalSpecs = null, bool $paged = true, ?int $limit = null): array
{
if(null == $additionalSpecs)
$additionalSpecs = [];
array_unshift($additionalSpecs, $spec);
array_walk($additionalSpecs, function($spec) use ($criteria): void {
$criteria->andX($spec);
if($spec instanceof ISelectIdListSpec)
$criteria->andX($spec->getIdListSpec());
});
return $this->profileRepository->listIdMatchingSpec(
$criteria,
$paged ? $this->getOffset() : 0,
$paged ? $this->perPage : $limit
// $limit ?: $this->perPage
);
}
protected function getPage(): int
{
$page = (int)$this->request->get('page');
if ($page < 1)
$page = 1;
return $page;
}
protected function getOffset(): float|int
{
return ($this->getPage() - 1) * $this->perPage;
}
protected function takeFakePage(int $totalResults, array $profiles): Page
{
//если передана страница, которой нет в основной выборке
//if($totalResults != 0 && $this->getPage() > ceil($totalResults / $this->perPage))
if($totalResults > 0 && $this->getPage() != 1 && $this->getOffset() > $totalResults - 1)
throw new NotFoundHttpException('Page number doesn\'t exist');
$profileIds = array_map(function(ProfileListingReadModel $profileListingReadModel): int {
return $profileListingReadModel->id;
}, $profiles);
$this->eventDispatcher->dispatch(new PaginatorPageTakenEvent($profiles), PaginatorPageTakenEvent::NAME);
return new FakeORMQueryPage($this->getOffset(), $this->getPage(), $this->perPage, $totalResults, $profiles);
}
public function listRandom(
City $city, ?string $country, ?Filter $spec, ?array $additionalSpecs, bool $active, ?bool $masseur = false,
array $genders = [Genders::FEMALE], bool $paged = true, ?int $limit = null
): array|Page
{
$masseurSpec = $this->getMasseurSpecByFlag($masseur);
$activeSpec = $this->getActiveSpecByFlag($active);
$criteria = Spec::andX(
$country ? ProfileIsLocatedInCountry::withCountryCode($city->getCountryCode()) : ProfileIsLocated::withinCity($city),
$activeSpec
);
if($masseurSpec)
$criteria->andX($masseurSpec);
$criteria->andX($this->getModerationSpecByFlag());
$criteria->andX(new ProfileHasOneOfGenders($genders));
$criteria->andX(new ProfileOrderedByRandom());
$idList = $this->listIdOfCriteriaWithCustomSpec($criteria, $spec, $additionalSpecs, $paged, $limit);
$profiles = [];
if(!empty($idList)) {
$profiles = $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList));
}
if($paged) {
$profiles = $this->takeFakePage(count($profiles)/*$this->perPage*/, $profiles);
}
return $profiles;
}
public function listRecent(City $city, int $count, array $genders = [Genders::FEMALE]): array
{
$criteria = Spec::andX(
//new ProfileAdBoardPlacement(),
new ProfileIsActive(),
new ProfilePlacementHiding(),
new \App\Specification\QueryModifier\ProfileAvatar(),
// убрано чтобы не было мало записей на маленьких городах
// new ProfileIsNew(),
ProfileIsLocated::withinCity($city),
new ProfileOrderedByCreated(),
new LimitResult($count)
);
$criteria->andX($this->getActiveSpecByFlag(true));
$criteria->andX($this->getModerationSpecByFlag());
$criteria->andX(new ProfileHasOneOfGenders($genders));
$result = $this->profileRepository->matchingSpecRaw($criteria, null, false);
return $result;
}
public function listBySpec(?Filter $spec, ?array $additionalSpecs = null, ?int $limit = null, bool $paged = true, ?callable $fetchByIdMethod = null): array|Page
{
$this->perPage = $limit ?? $this->perPageDefault;
$criteria = Spec::andX(
$spec
);
$criteriaForIdList = clone $criteria;
if($paged) {
$count = $this->countOfCriteriaWithCustomSpec($criteria, $spec, $additionalSpecs);
if(0 == $count)
return $this->takeFakePage($count, []);
}
$idList = $this->listIdOfCriteriaWithCustomSpec($criteriaForIdList, $spec, $additionalSpecs, $paged);
$profiles = [];
if(!empty($idList)) {
# $fetchByIdMethod передается если нужно, чтобы результат был не в виде ProfileListingReadModel, например.
# Как вариант - [$profileRepository, 'findByIds']
$profiles = null == $fetchByIdMethod
? $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList))
: $fetchByIdMethod($idList);
}
if($paged) {
$profiles = $this->takeFakePage($count, $profiles);
}
$this->restorePerPageToDefault();
return $profiles;
}
protected function getMasseurSpecByFlag(?bool $masseur): ?Specification
{
if(true === $masseur) {
$masseurSpec = new ProfileIsMasseur();
} else if(false === $masseur) {
$masseurSpec = new ProfileIsNotMasseur();
} else {
$masseurSpec = null;
}
return $masseurSpec;
}
public function getActiveSpecByFlag(bool $active): ProfileIsHidden|ProfileIsArchived|ProfileIsNotHidden|ProfileIsActive
{
if($active) {
$activeSpec = $this->features->free_profiles() ? new ProfileIsNotHidden() : new ProfileIsActive();
} else {
$activeSpec = $this->features->free_profiles() ? new ProfileIsHidden() : new ProfileIsArchived();
}
return $activeSpec;
}
public function getModerationSpecByFlag(): ProfileIsModerationPassed|ProfileIsNotRejected
{
return $this->features->hard_moderation() ? new ProfileIsModerationPassed() : new ProfileIsNotRejected();
}
protected function getOrderSpecByFlags(string $order, bool $active): string|ProfileOrderedByStatus|ProfileOrderedByUpdated|ProfileOrderedByInactivated|FreeProfilesFeatureProfileOrder|FreeProfilesFeatureArchivedProfileOrder
{
switch($order) {
case self::ORDER_BY_UPDATED:
$defaultOrder = new ProfileOrderedByUpdated();
break;
case self::ORDER_NONE:
$defaultOrder = null;
break;
case self::ORDER_BY_STATUS:
default:
$defaultOrder = new ProfileOrderedByStatus();
break;
}
if(null != $defaultOrder) {
if ($this->features->free_profiles()) {
$order = $active ? new FreeProfilesFeatureProfileOrder($this->features->consider_approved_priority()) : new FreeProfilesFeatureArchivedProfileOrder();
} else {
$order = $active ? $defaultOrder : new ProfileOrderedByInactivated();
}
}
return $order;
}
public function executedCountQueryData(): array
{
return $this->executedCountQueryData;
}
protected function restorePerPageToDefault(): void
{
$this->perPage = $this->perPageDefault;
}
}