src/Service/ProfileList.php line 124

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Bridge\Porpaginas\Doctrine\ORM\FakeORMQueryPage;
  4. use App\Entity\Location\City;
  5. use App\Entity\Profile\Genders;
  6. use App\Entity\Profile\Profile;
  7. use App\Event\PaginatorPageTakenEvent;
  8. use App\Repository\ProfileRepository;
  9. use App\Repository\ReadModel\ProfileListingReadModel;
  10. use App\Specification\Profile\ICountIdSpec;
  11. use App\Specification\Profile\ISelectIdListSpec;
  12. use App\Specification\Profile\ProfileHasOneOfGenders;
  13. use App\Specification\Profile\ProfileIdINOrderedByINValues;
  14. use App\Specification\Profile\ProfileIdNotIn;
  15. use App\Specification\Profile\ProfileIsActive;
  16. use App\Specification\Profile\ProfileIsArchived;
  17. use App\Specification\Profile\ProfileIsLocatedInCountry;
  18. use App\Specification\Profile\ProfileIsLocated;
  19. use App\Specification\Profile\ProfileIsMasseur;
  20. use App\Specification\Profile\ProfileIsModerationPassed;
  21. use App\Specification\Profile\ProfileIsNotMasseur;
  22. use App\Specification\Profile\ProfileIsNotRejected;
  23. use App\Specification\QueryModifier\FreeProfilesFeatureArchivedProfileOrder;
  24. use App\Specification\QueryModifier\FreeProfilesFeatureProfileOrder;
  25. use App\Specification\Profile\ProfileIsHidden;
  26. use App\Specification\Profile\ProfileIsNotHidden;
  27. use App\Specification\QueryModifier\LimitResult;
  28. use App\Specification\QueryModifier\ProfileOrderedByCreated;
  29. use App\Specification\QueryModifier\ProfileOrderedByInactivated;
  30. use App\Specification\QueryModifier\ProfileOrderedByRandom;
  31. use App\Specification\QueryModifier\ProfileOrderedByUpdated;
  32. use App\Specification\QueryModifier\ProfileOrderedByStatus;
  33. use App\Specification\QueryModifier\ProfilePlacementHiding;
  34. use Doctrine\ORM\EntityManagerInterface;
  35. use Happyr\DoctrineSpecification\Filter\Filter;
  36. use Happyr\DoctrineSpecification\Logic\AndX;
  37. use Happyr\DoctrineSpecification\Spec;
  38. use Happyr\DoctrineSpecification\Specification\Specification;
  39. use Porpaginas\Page;
  40. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  41. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  42. use Symfony\Component\HttpFoundation\Request;
  43. use Symfony\Component\HttpFoundation\RequestStack;
  44. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  45. class ProfileList
  46. {
  47.     public const ORDER_BY_STATUS 'status';
  48.     public const ORDER_BY_UPDATED 'updated';
  49.     public const ORDER_NONE 'none';
  50.     private ?Request $request;
  51.     private int $perPageDefault;
  52.     private int $perPage;
  53.     private array $executedCountQueryData;
  54.     private bool $withTopPlacementForPagination false;
  55.     public function __construct(
  56.         private ProfileRepository $profileRepository,
  57.         RequestStack $requestStack,
  58.         private Features $features,
  59.         ParameterBagInterface $parameterBag,
  60.         private EventDispatcherInterface $eventDispatcher,
  61.         private EntityManagerInterface $entityManager,
  62.         private ProfileTopBoard $profileTopBoard,
  63.         private SplitFirstPagePagination $splitFirstPagePagination,
  64.     )
  65.     {
  66.         $this->request $requestStack->getCurrentRequest();
  67.         $this->perPageDefault intval($parameterBag->get('profile_list.per_page'));
  68.         $this->perPage $this->perPageDefault;
  69.     }
  70.     public function listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacement(
  71.         City $city, ?Filter $spec, array $additionalSpecs null, array $genders = [Genders::FEMALE]
  72.     ): array|Page
  73.     {
  74.         return $this->listActiveWithinCityOrderedByStatusWithSpec($city$spec$additionalSpecs$genderstrue);
  75.     }
  76.     public function listActiveWithinCityOrderedByStatusWithSpec(
  77.         City $city, ?Filter $spec, array $additionalSpecs null, array $genders = [Genders::FEMALE], bool $avoidTopPlacement false
  78.     ): array|Page
  79.     {
  80.         return $this->list($citynull$spec$additionalSpecstruenullself::ORDER_BY_STATUS,
  81.             nulltruenull$genders$avoidTopPlacement);
  82.     }
  83.     public function listActiveWithinCityOrderedByStatusWithSpecLimited(
  84.         City $city, ?Filter $spec, array $additionalSpecs null, array $genders = [Genders::FEMALE], bool $avoidTopPlacement falseint $limit 0,
  85.     ): array|Page
  86.     {
  87.         return $this->list($citynull$spec$additionalSpecstruenullself::ORDER_BY_STATUS,
  88.             $limitfalsenull$genders$avoidTopPlacement);
  89.     }
  90.     public function listActiveNotMasseurWithinCityOrderedByStatusWithSpec(
  91.         City $city, ?Filter $spec, array $additionalSpecs null, array $genders = [Genders::FEMALE]
  92.     ): array|Page
  93.     {
  94.         return $this->list($citynull$spec$additionalSpecstruefalseself::ORDER_BY_STATUS,
  95.             nulltruenull$genders);
  96.     }
  97.     public function countActiveWithinCityWithSpec(
  98.         City $city, ?Filter $spec, array $additionalSpecs null, array $genders = [Genders::FEMALE]
  99.     ): int
  100.     {
  101.         $criteria Spec::andX(
  102.             ProfileIsLocated::withinCity($city),
  103.             $this->getActiveSpecByFlag(true)
  104.         );
  105.         $criteria->andX($this->getModerationSpecByFlag());
  106.         $criteria->andX(new ProfileHasOneOfGenders($genders));
  107.         return $this->countOfCriteriaWithCustomSpec($criteria$spec$additionalSpecs);
  108.     }
  109.     /**
  110.      * @param Profile|bool|null $avoidOrTopPlacement Принимает bool для обратной совместимости с старым кодом
  111.      */
  112.     public function list(
  113.         City $city, ?string $country, ?Filter $spec, ?array $additionalSpecsbool $active, ?bool $masseur false,
  114.         ?string $order self::ORDER_BY_STATUS, ?int $limit nullbool $paged true, ?callable $fetchByIdMethod null,
  115.         array $genders = [Genders::FEMALE], Profile|null|bool $avoidOrTopPlacement false
  116.     ): array|Page
  117.     {
  118. //        $this->perPage = $limit ?? $this->perPageDefault;
  119.         $this->perPage $this->perPageDefault;
  120.         $countProfiles 0;
  121.         $returnProfiles = [];
  122.         $topPlacementToAvoidId null;
  123.         if (/*null === $limit && */true === $avoidOrTopPlacement) {
  124.             $profileTopPlacement $this->profileTopBoard->topPlacementSatisfiedBy($city$spec);
  125.             if (null !== $profileTopPlacement) {
  126.                 $topPlacementToAvoidId $profileTopPlacement->getId();
  127.                 $countProfiles 1;
  128.                 $returnProfiles[] = $profileTopPlacement;
  129.             }
  130.         } elseif ($avoidOrTopPlacement instanceof Profile) {
  131.             $topPlacementToAvoidId $avoidOrTopPlacement->getId();
  132.             $countProfiles 1;
  133.             $returnProfiles[] = $avoidOrTopPlacement;
  134.         }
  135.         $this->withTopPlacementForPagination null !== $topPlacementToAvoidId;
  136.         if ($this->withTopPlacementForPagination && !$this->splitFirstPagePagination->isActive()) {
  137.             //на тесте часто ставят 1 на страницу, что ломает логику, т.к. после декремента становится 0 на страницу,
  138.             //за тем и условие
  139.             if ($this->perPage 1) {
  140.                 $this->perPage--;
  141.             }
  142.         }
  143.         $order $this->getOrderSpecByFlags($order$active);
  144.         $masseurSpec $this->getMasseurSpecByFlag($masseur);
  145.         $activeSpec $this->getActiveSpecByFlag($active);
  146.         $excludeTopProfileSpec null !== $topPlacementToAvoidId ? new ProfileIdNotIn([$topPlacementToAvoidId]) : null;
  147.         $criteria Spec::andX(
  148.             $country ProfileIsLocatedInCountry::withCountryCode($city->getCountryCode()) : ProfileIsLocated::withinCity($city),
  149.             $activeSpec
  150.         );
  151.         if ($masseurSpec) {
  152.             $criteria->andX($masseurSpec);
  153.         }
  154.         if (null !== $excludeTopProfileSpec) {
  155.             $criteria->andX($excludeTopProfileSpec);
  156.         }
  157.         $criteria->andX($this->getModerationSpecByFlag());
  158.         $criteria->andX(new ProfileHasOneOfGenders($genders));
  159.         $criteriaForIdList = clone $criteria;
  160.         if ($paged) {
  161.             $pagedCount $this->countOfCriteriaWithCustomSpec($criteria$spec$additionalSpecs);
  162.             $countProfiles += $pagedCount;
  163.             if (=== $pagedCount) {
  164.                 return $this->takeFakePage($countProfiles$returnProfiles);
  165.             }
  166.         }
  167.         if ($order) {
  168.             $criteriaForIdList->andX($order);
  169.         }
  170.         $idList $this->listIdOfCriteriaWithCustomSpec($criteriaForIdList$spec$additionalSpecs$paged$limit);
  171.         $profiles = [];
  172.         if(!empty($idList)) {
  173.             // $profiles = $this->profileRepository->matchingSpecRaw(new ProfileIdINOrderedByINValues($idList), null, true, array($this->profileRepository, 'hydrateProfileRow'));
  174.             $profiles null === $fetchByIdMethod
  175.                 $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList))
  176.                 : $fetchByIdMethod(new ProfileIdINOrderedByINValues($idList));
  177.         }
  178.         $returnProfiles array_merge($returnProfiles$profiles);
  179.         if ($paged) {
  180.             $returnProfiles $this->takeFakePage($countProfiles$returnProfiles);
  181.         }
  182.         $this->restorePerPageToDefault();
  183.         return $returnProfiles;
  184.     }
  185.     public function listForMap(
  186.         City $city, ?string $country, ?Filter $spec, ?array $additionalSpecsbool $active, ?bool $masseur false,
  187.         ?string $order self::ORDER_BY_STATUS, array $genders = [Genders::FEMALE], int $coordsRoundPrecision 3,
  188.     ): array|Page
  189.     {
  190.         $order $this->getOrderSpecByFlags($order$active);
  191.         $masseurSpec $this->getMasseurSpecByFlag($masseur);
  192.         $activeSpec $this->getActiveSpecByFlag($active);
  193.         $criteria Spec::andX(
  194.             $country ProfileIsLocatedInCountry::withCountryCode($city->getCountryCode()) : ProfileIsLocated::withinCity($city),
  195.             //$activeSpec
  196.         );
  197.         if($masseurSpec)
  198.             $criteria->andX($masseurSpec);
  199.         $criteria->andX($this->getModerationSpecByFlag());
  200.         $criteria->andX(new ProfileHasOneOfGenders($genders));
  201.         if(null == $additionalSpecs)
  202.             $additionalSpecs = [];
  203.         array_unshift($additionalSpecs$spec);
  204.         array_walk($additionalSpecs, function($spec) use ($criteria): void {
  205.             $criteria->andX($spec);
  206.         });
  207.         $profiles $this->profileRepository->listForMapMatchingSpec($criteria$coordsRoundPrecision);
  208.         return $profiles;
  209.     }
  210.     protected function countOfCriteriaWithCustomSpec(AndX $criteria, ?Filter $spec, array $additionalSpecs null): int
  211.     {
  212.         if(null == $additionalSpecs)
  213.             $additionalSpecs = [];
  214.         array_unshift($additionalSpecs$spec);
  215.         array_walk($additionalSpecs, function($spec) use ($criteria): void {
  216.             $criteria->andX($spec);
  217.             if($spec instanceof ICountIdSpec)
  218.                 $criteria->andX($spec->getCountIdSpec());
  219.         });
  220.         $defaultStack $this->entityManager->getConnection()->getConfiguration()->getSQLLogger();
  221.         $stack = new \Doctrine\DBAL\Logging\DebugStack();
  222.         $this->entityManager->getConnection()->getConfiguration()->setSQLLogger($stack);
  223.         $result $this->profileRepository->countMatchingSpec($criteria);
  224.         //$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' => []];//
  225.         $this->executedCountQueryData $stack->queries[count($stack->queries)];
  226.         $this->entityManager->getConnection()->getConfiguration()->setSQLLogger($defaultStack);
  227.         return $result;
  228.     }
  229.     protected function listIdOfCriteriaWithCustomSpec(AndX $criteria, ?Filter $spec, array $additionalSpecs nullbool $paged true, ?int $limit null): array
  230.     {
  231.         if(null == $additionalSpecs)
  232.             $additionalSpecs = [];
  233.         array_unshift($additionalSpecs$spec);
  234.         array_walk($additionalSpecs, function($spec) use ($criteria): void {
  235.             $criteria->andX($spec);
  236.             if($spec instanceof ISelectIdListSpec)
  237.                 $criteria->andX($spec->getIdListSpec());
  238.         });
  239.         return $this->profileRepository->listIdMatchingSpec(
  240.             $criteria,
  241.             $paged $this->getOffset() : 0,
  242.             $paged $this->getPerPageForQuery() : $limit
  243. //            $limit ?: $this->perPage
  244.         );
  245.     }
  246.     protected function getPage(): int
  247.     {
  248.         $page = (int)$this->request->get('page');
  249.         if ($page 1)
  250.             $page 1;
  251.         return $page;
  252.     }
  253.     protected function getOffset(): float|int
  254.     {
  255.         if ($this->splitFirstPagePagination->isActive()) {
  256.             return $this->splitFirstPagePagination->getOffsetForPage(
  257.                 $this->getPage(),
  258.                 $this->withTopPlacementForPagination
  259.             );
  260.         }
  261.         return ($this->getPage() - 1) * $this->perPage;
  262.     }
  263.     private function getPerPageForQuery(): int
  264.     {
  265.         if ($this->splitFirstPagePagination->isActive()) {
  266.             return $this->splitFirstPagePagination->getLimitForPage(
  267.                 $this->getPage(),
  268.                 $this->withTopPlacementForPagination
  269.             );
  270.         }
  271.         return $this->perPage;
  272.     }
  273.     protected function takeFakePage(int $totalResults, array $profiles): Page
  274.     {
  275.         if ($this->splitFirstPagePagination->isActive()) {
  276.             if ($this->splitFirstPagePagination->isPageOutOfRange($this->getPage(), $totalResults)) {
  277.                 throw new NotFoundHttpException('Page number doesn\'t exist');
  278.             }
  279.             $this->eventDispatcher->dispatch(new PaginatorPageTakenEvent($profiles), PaginatorPageTakenEvent::NAME);
  280.             return new FakeORMQueryPage(
  281.                 $this->splitFirstPagePagination->getDisplayOffset($this->getPage()),
  282.                 $this->getPage(),
  283.                 $this->splitFirstPagePagination->getDisplayLimit($this->getPage()),
  284.                 $totalResults,
  285.                 $profiles
  286.             );
  287.         }
  288.         //если передана страница, которой нет в основной выборке
  289.         //if($totalResults != 0 && $this->getPage() > ceil($totalResults / $this->perPage))
  290.         if ($totalResults && $this->getPage() !== && $this->getOffset() > $totalResults 1) {
  291.             throw new NotFoundHttpException('Page number doesn\'t exist');
  292.         }
  293.         //$profileIds = array_map(function(ProfileListingReadModel $profileListingReadModel): int {
  294.         //    return $profileListingReadModel->id;
  295.         //}, $profiles);
  296.         $this->eventDispatcher->dispatch(new PaginatorPageTakenEvent($profiles), PaginatorPageTakenEvent::NAME);
  297.         return new FakeORMQueryPage($this->getOffset(), $this->getPage(), $this->perPage$totalResults$profiles);
  298.     }
  299.     public function listRandom(
  300.         City $city, ?string $country, ?Filter $spec, ?array $additionalSpecsbool $active, ?bool $masseur false,
  301.         array $genders = [Genders::FEMALE], bool $paged true, ?int $limit null
  302.     ): array|Page
  303.     {
  304.         $masseurSpec $this->getMasseurSpecByFlag($masseur);
  305.         $activeSpec $this->getActiveSpecByFlag($active);
  306.         $criteria Spec::andX(
  307.             $country ProfileIsLocatedInCountry::withCountryCode($city->getCountryCode()) : ProfileIsLocated::withinCity($city),
  308.             $activeSpec
  309.         );
  310.         if($masseurSpec)
  311.             $criteria->andX($masseurSpec);
  312.         $criteria->andX($this->getModerationSpecByFlag());
  313.         $criteria->andX(new ProfileHasOneOfGenders($genders));
  314.         $criteria->andX(new ProfileOrderedByRandom());
  315.         $idList $this->listIdOfCriteriaWithCustomSpec($criteria$spec$additionalSpecs$paged$limit);
  316.         $profiles = [];
  317.         if(!empty($idList)) {
  318.             $profiles $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList));
  319.         }
  320.         if($paged) {
  321.             $profiles $this->takeFakePage(count($profiles)/*$this->perPage*/$profiles);
  322.         }
  323.         return $profiles;
  324.     }
  325.     public function listRecent(City $cityint $count, array $genders = [Genders::FEMALE]): array
  326.     {
  327.         $criteria Spec::andX(
  328.             //new ProfileAdBoardPlacement(),
  329.             new ProfileIsActive(),
  330.             new ProfilePlacementHiding(),
  331.             new \App\Specification\QueryModifier\ProfileAvatar(),
  332.             // убрано чтобы не было мало записей на маленьких городах
  333.             // new ProfileIsNew(),
  334.             ProfileIsLocated::withinCity($city),
  335.             new ProfileOrderedByCreated(),
  336.             new LimitResult($count)
  337.         );
  338.         $criteria->andX($this->getActiveSpecByFlag(true));
  339.         $criteria->andX($this->getModerationSpecByFlag());
  340.         $criteria->andX(new ProfileHasOneOfGenders($genders));
  341.         $result $this->profileRepository->matchingSpecRaw($criterianullfalse);
  342.         return $result;
  343.     }
  344.     public function listBySpec(?Filter $spec, ?array $additionalSpecs null, ?int $limit nullbool $paged true, ?callable $fetchByIdMethod null): array|Page
  345.     {
  346.         $this->perPage $limit ?? $this->perPageDefault;
  347.         $criteria Spec::andX(
  348.             $spec
  349.         );
  350.         $criteriaForIdList = clone $criteria;
  351.         if($paged) {
  352.             $count $this->countOfCriteriaWithCustomSpec($criteria$spec$additionalSpecs);
  353.             if(== $count)
  354.                 return $this->takeFakePage($count, []);
  355.         }
  356.         $idList $this->listIdOfCriteriaWithCustomSpec($criteriaForIdList$spec$additionalSpecs$paged);
  357.         $profiles = [];
  358.         if(!empty($idList)) {
  359.             # $fetchByIdMethod передается если нужно, чтобы результат был не в виде ProfileListingReadModel, например.
  360.             # Как вариант - [$profileRepository, 'findByIds']
  361.             $profiles null == $fetchByIdMethod
  362.                 $this->profileRepository->fetchListingByIds(new ProfileIdINOrderedByINValues($idList))
  363.                 : $fetchByIdMethod($idList);
  364.         }
  365.         if($paged) {
  366.             $profiles $this->takeFakePage($count$profiles);
  367.         }
  368.         $this->restorePerPageToDefault();
  369.         return $profiles;
  370.     }
  371.     protected function getMasseurSpecByFlag(?bool $masseur): ?Specification
  372.     {
  373.         if(true === $masseur) {
  374.             $masseurSpec = new ProfileIsMasseur();
  375.         } else if(false === $masseur) {
  376.             $masseurSpec = new ProfileIsNotMasseur();
  377.         } else {
  378.             $masseurSpec null;
  379.         }
  380.         return $masseurSpec;
  381.     }
  382.     public function getActiveSpecByFlag(bool $active): ProfileIsHidden|ProfileIsArchived|ProfileIsNotHidden|ProfileIsActive
  383.     {
  384.         if($active) {
  385.             $activeSpec $this->features->free_profiles() ? new ProfileIsNotHidden() : new ProfileIsActive();
  386.         } else {
  387.             $activeSpec $this->features->free_profiles() ? new ProfileIsHidden() : new ProfileIsArchived();
  388.         }
  389.         return $activeSpec;
  390.     }
  391.     public function getModerationSpecByFlag(): ProfileIsModerationPassed|ProfileIsNotRejected
  392.     {
  393.         return $this->features->hard_moderation() ? new ProfileIsModerationPassed() : new ProfileIsNotRejected();
  394.     }
  395.     protected function getOrderSpecByFlags(string $orderbool $active): string|ProfileOrderedByStatus|ProfileOrderedByUpdated|ProfileOrderedByInactivated|FreeProfilesFeatureProfileOrder|FreeProfilesFeatureArchivedProfileOrder
  396.     {
  397.         switch($order) {
  398.             case self::ORDER_BY_UPDATED:
  399.                 $defaultOrder = new ProfileOrderedByUpdated();
  400.                 break;
  401.             case self::ORDER_NONE:
  402.                 $defaultOrder null;
  403.                 break;
  404.             case self::ORDER_BY_STATUS:
  405.             default:
  406.                 $defaultOrder = new ProfileOrderedByStatus();
  407.                 break;
  408.         }
  409.         if(null != $defaultOrder) {
  410.             if ($this->features->free_profiles()) {
  411.                 $order $active ? new FreeProfilesFeatureProfileOrder($this->features->consider_approved_priority()) : new FreeProfilesFeatureArchivedProfileOrder();
  412.             } else {
  413.                 $order $active $defaultOrder : new ProfileOrderedByInactivated();
  414.             }
  415.         }
  416.         return $order;
  417.     }
  418.     public function executedCountQueryData(): array
  419.     {
  420.         return $this->executedCountQueryData;
  421.     }
  422.     protected function restorePerPageToDefault(): void
  423.     {
  424.         $this->perPage $this->perPageDefault;
  425.         $this->withTopPlacementForPagination false;
  426.     }
  427.   
  428.     public function getProfilesByIdsInOrder(array $ids): array
  429.     {
  430.         if (empty($ids)) {
  431.             return [];
  432.         }
  433.         return $this->profileRepository->fetchListingByIds(new \App\Specification\Profile\ProfileIdINOrderedByINValues($ids));
  434.     }
  435.     public function getActiveProfilesByIdsInOrderWithinCityAndSpec(
  436.         City $city,
  437.         array $ids,
  438.         ?Filter $spec,
  439.         ?array $additionalSpecs null,
  440.         array $genders = [Genders::FEMALE],
  441.     ): array {
  442.         if (empty($ids)) {
  443.             return [];
  444.         }
  445.         $additionalSpecs ??= [];
  446.         $additionalSpecs[] = new ProfileIdINOrderedByINValues($ids);
  447.         return $this->list(
  448.             $city,
  449.             null,
  450.             $spec,
  451.             $additionalSpecs,
  452.             true,
  453.             null,
  454.             self::ORDER_NONE,
  455.             count($ids),
  456.             false,
  457.             null,
  458.             $genders,
  459.         );
  460.     }
  461.    
  462.     public function createFakePageFromProfiles(int $totalResults, array $profiles): Page
  463.     {
  464.         return $this->takeFakePage($totalResults$profiles);
  465.     }
  466. }