src/Controller/ProfileListController.php line 755

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-03-19
  5.  * Time: 22:28
  6.  */
  7. namespace App\Controller;
  8. use App\Bridge\Porpaginas\Doctrine\ORM\FakeORMQueryPage;
  9. use App\Entity\Location\City;
  10. use App\Entity\Location\County;
  11. use App\Entity\Location\District;
  12. use App\Entity\Location\Station;
  13. use App\Entity\Profile\BodyTypes;
  14. use App\Entity\Profile\BreastTypes;
  15. use App\Entity\Profile\Genders;
  16. use App\Entity\Profile\HairColors;
  17. use App\Entity\Profile\Nationalities;
  18. use App\Entity\Profile\PrivateHaircuts;
  19. use App\Entity\Service;
  20. use App\Entity\ServiceGroups;
  21. use App\Entity\TakeOutLocations;
  22. use App\Repository\ServiceRepository;
  23. use App\Repository\StationRepository;
  24. use App\Service\CountryCurrencyResolver;
  25. use App\Service\DefaultCityProvider;
  26. use App\Service\Features;
  27. use App\Service\ListingRotationApi;
  28. use App\Service\ListingService;
  29. use App\Service\ProfileList;
  30. use App\Service\ProfileListingDataCreator;
  31. use App\Service\ProfileListSpecificationService;
  32. use App\Service\ProfileFilterService;
  33. use App\Service\ProfileTopBoard;
  34. use App\Specification\ElasticSearch\ISpecification;
  35. use App\Specification\Profile\ProfileHasApartments;
  36. use App\Specification\Profile\ProfileHasComments;
  37. use App\Specification\Profile\ProfileHasVideo;
  38. use App\Specification\Profile\ProfileIdIn;
  39. use App\Specification\Profile\ProfileIdINOrderedByINValues;
  40. use App\Specification\Profile\ProfileIdNotIn;
  41. use App\Specification\Profile\ProfileIsApproved;
  42. use App\Specification\Profile\ProfileIsElite;
  43. use App\Specification\Profile\ProfileIsLocated;
  44. use App\Specification\Profile\ProfileIsProvidingOneOfServices;
  45. use App\Specification\Profile\ProfileIsProvidingTakeOut;
  46. use App\Specification\Profile\ProfileWithAge;
  47. use App\Specification\Profile\ProfileWithBodyType;
  48. use App\Specification\Profile\ProfileWithBreastType;
  49. use App\Specification\Profile\ProfileWithHairColor;
  50. use App\Specification\Profile\ProfileWithNationality;
  51. use App\Specification\Profile\ProfileWithApartmentsOneHourPrice;
  52. use App\Specification\Profile\ProfileWithPrivateHaircut;
  53. use Flagception\Bundle\FlagceptionBundle\Annotations\Feature;
  54. use Happyr\DoctrineSpecification\Filter\Filter;
  55. use Happyr\DoctrineSpecification\Logic\OrX;
  56. use Porpaginas\Doctrine\ORM\ORMQueryResult;
  57. use Porpaginas\Page;
  58. use Psr\Cache\CacheItemPoolInterface;
  59. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
  60. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
  61. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  62. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  63. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  64. use Symfony\Component\HttpFoundation\Request;
  65. use Happyr\DoctrineSpecification\Spec;
  66. use Symfony\Component\HttpFoundation\RequestStack;
  67. use Symfony\Component\HttpFoundation\Response;
  68. /**
  69.  * @see \App\Console\Export\ExportRotationListingsConfigCommand for listing API endpoints
  70.  */
  71. #[Cache(maxage60, public: true)]
  72. class ProfileListController extends AbstractController
  73. {
  74.     use ExtendedPaginationTrait;
  75.     use SpecTrait;
  76.     use ProfileMinPriceTrait;
  77.     use ResponseTrait;
  78.     const ENTRIES_ON_PAGE 36;
  79.     const RESULT_SOURCE_COUNTY 'county';
  80.     const RESULT_SOURCE_DISTRICT 'district';
  81.     const RESULT_SOURCE_STATION 'station';
  82.     const RESULT_SOURCE_APPROVED 'approved';
  83.     const RESULT_SOURCE_WITH_COMMENTS 'with_comments';
  84.     const RESULT_SOURCE_WITH_VIDEO 'with_video';
  85.     const RESULT_SOURCE_WITH_SELFIE 'with_selfie';
  86.     const RESULT_SOURCE_ELITE 'elite';
  87.     const RESULT_SOURCE_MASSEURS 'masseurs';
  88.     const RESULT_SOURCE_MASSAGE_SERVICE 'massage_service';
  89.     const RESULT_SOURCE_BY_PARAMS 'by_params';
  90.     const RESULT_SOURCE_SERVICE 'service';
  91.     const RESULT_SOURCE_CITY 'city';
  92.     const RESULT_SOURCE_COUNTRY 'country';
  93.     const CACHE_ITEM_STATION_ADDED_PROFILES 'station_added_profiles_ids_';
  94.     private ?string $source null;
  95.     public function __construct(
  96.         private RequestStack $requestStack,
  97.         private ProfileList                     $profileList,
  98.         private CountryCurrencyResolver         $countryCurrencyResolver,
  99.         private ServiceRepository               $serviceRepository,
  100.         private ListingService                  $listingService,
  101.         private Features                        $features,
  102.         private ProfileFilterService            $profilesFilterService,
  103.         private ProfileListSpecificationService $profileListSpecificationService,
  104.         private ProfileListingDataCreator       $profileListingDataCreator,
  105.         private CacheItemPoolInterface          $stationAddedProfilesCache,
  106.         private ParameterBagInterface           $parameterBag,
  107.         private ListingRotationApi              $listingRotationApi,
  108.         private ProfileTopBoard                 $profileTopBoard,
  109.     ) {}
  110.     /**
  111.      * @Feature("has_masseurs")
  112.      */
  113.     #[ParamConverter("city"converter"city_converter")]
  114.     public function listForMasseur(City $cityServiceRepository $serviceRepository): Response
  115.     {
  116.         $specs $this->profileListSpecificationService->listForMasseur($city);
  117.         $response = new Response();
  118.         $massageGroupServices $serviceRepository->findBy(['group' => ServiceGroups::MASSAGE]);
  119.         $alternativeSpec $this->getORSpecForItemsArray([$massageGroupServices], function($item): ProfileIsProvidingOneOfServices {
  120.             return new ProfileIsProvidingOneOfServices($item);
  121.         });
  122.         $result $this->paginatedListing($city'/city/{city}/masseur', ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_MASSAGE_SERVICE$response);
  123.         return $this->render('ProfileList/list.html.twig', [
  124.             'profiles' => $result,
  125.             'source' => $this->source,
  126.             'source_default' => self::RESULT_SOURCE_MASSEURS,
  127.             'recommendationSpec' => $specs->recommendationSpec(),
  128.         ], response$response);
  129.     }
  130.     public function listByDefaultCity(ParameterBagInterface $parameterBagRequest $request): Response
  131.     {
  132.         $controller get_class($this).'::listByCity';
  133.         $path = [
  134.             'city' => $parameterBag->get('default_city'),
  135.             'subRequest' => true,
  136.         ];
  137.         //чтобы в обработчике можно было понять, по какому роуту зашли
  138.         $request->request->set('_route''profile_list.list_by_city');
  139.         return $this->forward($controller$path);
  140.     }
  141.     private function paginatedListing(City $city, ?string $apiEndpoint, array $apiParams, ?Filter $listingSpec null, ?OrX $alternativeSpec null, ?string $alternativeSource null, ?Response $response null): Page
  142.     {
  143.         $topPlacement $this->profileTopBoard->topPlacementSatisfiedBy($city$listingSpec);
  144.         $topPlacement?->setTopCard(); // mark as top card for UI
  145.         $page $this->getCurrentPageNumber();
  146.         $apiParams['city'] = $city->getId();
  147.         try {
  148.             if (null === $apiEndpoint) {
  149.                 throw new \RuntimeException('Empty API endpoint to switch to legacy listing query.');
  150.             }
  151.             $result $this->listingRotationApi->paginate($apiEndpoint$apiParams$page$topPlacement);
  152.             $response?->setMaxAge(10);
  153.         } catch (\Exception) {
  154.             $avoidOrTopPlacement = (null !== $topPlacement && $page 2) ? $topPlacement null;
  155.             $result $this->profileList->list($citynull$listingSpec, [], truenullProfileList::ORDER_BY_STATUS,
  156.                 nulltruenull, [Genders::FEMALE], $avoidOrTopPlacement);
  157.         }
  158.         if (null !== $alternativeSpec || null !== $alternativeSource) {
  159.             $prevCount $result->count();
  160.             $result $this->checkEmptyResultNotMasseur($result$city$alternativeSpec$alternativeSource);
  161.             if ($result->count() > $prevCount) {
  162.                 $response?->setMaxAge(60);
  163.             }
  164.         }
  165.         return $result;
  166.     }
  167.     #[ParamConverter("city"converter"city_converter")]
  168.     public function listByCity(ParameterBagInterface $parameterBagRequest $requestCity $citybool $subRequest false): Response
  169.     {
  170.         $page $this->getCurrentPageNumber();
  171.         if ($this->features->redirect_default_city_to_homepage() && false === $subRequest && $city->equals($parameterBag->get('default_city')) && $page 2) {
  172.             return $this->redirectToRoute('homepage', [], 301);
  173.         }
  174.         $specs $this->profileListSpecificationService->listByCity();
  175.         $response = new Response();
  176.         $result $this->paginatedListing($city'/city/{city}', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  177.         return $this->render('ProfileList/list.html.twig', [
  178.             'profiles' => $result,
  179.             'recommendationSpec' => $specs->recommendationSpec(),
  180.         ], response$response);
  181.     }
  182.     /**
  183.      * @Feature("intim_moscow_listing")
  184.      */
  185.     #[Cache(maxage3600, public: true)]
  186.     public function listIntim(DefaultCityProvider $defaultCityProvider): Response
  187.     {
  188.         $city $defaultCityProvider->getDefaultCity();
  189.         $request $this->requestStack->getCurrentRequest();
  190.         $request?->attributes->set('city'$city);
  191.         $specs $this->profileListSpecificationService->listByCity();
  192.         $response = new Response();
  193.         $result $this->paginatedListing($city'/city/{city}/intim', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  194.         $result $this->shuffleProfilesOnPage($result);
  195.         $response->setMaxAge(3600);
  196.         return $this->render('ProfileList/list.html.twig', [
  197.             'profiles' => $result,
  198.             'city' => $city,
  199.             'recommendationSpec' => $specs->recommendationSpec(),
  200.         ], response$response);
  201.     }
  202.     #[ParamConverter("city"converter"city_converter")]
  203.     #[Entity("county"expr"repository.ofUriIdentityWithinCity(county, city)")]
  204.     public function listByCounty(Request $requestCity $cityCounty $county): Response
  205.     {
  206.         if (!$city->hasCounty($county)) {
  207.             throw $this->createNotFoundException();
  208.         }
  209.         $specs $this->profileListSpecificationService->listByCounty($county);
  210.         $response = new Response();
  211.         $alternativeSpec Spec::orX(ProfileIsLocated::withinCounties($city$city->getCounties()->toArray()));
  212.         $result $this->paginatedListing($city'/city/{city}/county/{county}', ['city' => $city->getId(), 'county' => $county->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_COUNTY$response);
  213.         return $this->render('ProfileList/list.html.twig', [
  214.             'profiles' => $result,
  215.             'source' => $this->source,
  216.             'source_default' => self::RESULT_SOURCE_COUNTY,
  217.             'county' => $county,
  218.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  219.                 'city' => $city->getUriIdentity(),
  220.                 'county' => $county->getUriIdentity(),
  221.                 'page' => $this->getCurrentPageNumber()
  222.             ]),
  223.             'recommendationSpec' => $specs->recommendationSpec(),
  224.         ], response$response);
  225.     }
  226.     #[ParamConverter("city"converter"city_converter")]
  227.     #[Entity("district"expr"repository.ofUriIdentityWithinCity(district, city)")]
  228.     public function listByDistrict(Request $requestCity $cityDistrict $district): Response
  229.     {
  230.         if (!$city->hasDistrict($district)) {
  231.             throw $this->createNotFoundException();
  232.         }
  233.         $specs $this->profileListSpecificationService->listByDistrict($district);
  234.         $response = new Response();
  235.         $alternativeSpec Spec::orX(ProfileIsLocated::withinDistricts($city$city->getDistricts()->toArray()));
  236.         $result $this->paginatedListing($city'/city/{city}/district/{district}', ['city' => $city->getId(), 'district' => $district->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_DISTRICT$response);
  237.         return $this->render('ProfileList/list.html.twig', [
  238.             'profiles' => $result,
  239.             'source' => $this->source,
  240.             'source_default' => self::RESULT_SOURCE_DISTRICT,
  241.             'district' => $district,
  242.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  243.                 'city' => $city->getUriIdentity(),
  244.                 'district' => $district->getUriIdentity(),
  245.                 'page' => $this->getCurrentPageNumber()
  246.             ]),
  247.             'recommendationSpec' => $specs->recommendationSpec(),
  248.         ], response$response);
  249.     }
  250.     /**
  251.      * @Feature("extra_category_without_prepayment")
  252.      */
  253.     #[ParamConverter("city"converter"city_converter")]
  254.     public function listWithoutPrepayment(Request $requestCity $city): Response
  255.     {
  256.         $listingData $this->profileListingDataCreator->getListingDataForFilter('listWithoutPrepayment', [], $city);
  257.         $specs $listingData['specs'];
  258.         $listingTypeName $listingData['listingTypeName'];
  259.         $response = new Response();
  260.         $result $this->paginatedListing($city'/city/{city}/category/without_prepayment', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  261.         $request->attributes->set('profiles_count'$result->count());
  262.         $request->attributes->set('listingTypeName'$listingTypeName);
  263.         $request->attributes->set('city'$city);
  264.         return $this->render('ProfileList/list.html.twig', [
  265.             'profiles' => $result,
  266.             'source' => $this->source,
  267.             'source_default' => 'without_prepayment',
  268.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  269.                 'city' => $city->getUriIdentity(),
  270.                 'page' => $this->getCurrentPageNumber(),
  271.             ]),
  272.             'recommendationSpec' => $specs->recommendationSpec(),
  273.         ], response$response);
  274.     }
  275.     #[ParamConverter("city"converter"city_converter")]
  276.     #[Entity("station"expr"repository.ofUriIdentityWithinCity(station, city)")]
  277.     public function listByStation(Request $requestCity $cityStation $station): Response
  278.     {
  279.         if (!$city->hasStation($station)) {
  280.             throw $this->createNotFoundException();
  281.         }
  282.         $specs $this->profileListSpecificationService->listByStation($station);
  283.         $response = new Response();
  284.         $result $this->paginatedListing($city'/city/{city}/station/{station}', ['city' => $city->getId(), 'station' => $station->getId()], $specs->spec(), nullnull$response);
  285.         $prevCount $result->count();
  286.         if (true === $this->features->station_page_add_profiles()) {
  287.             $spread $this->parameterBag->get('app.profile.station_page.added_profiles.spread');
  288.             $result $this->addSinglePageStationResults($result$city$station$spread ?: 5);
  289.         }
  290.         if (null !== $station->getDistrict()) {
  291.             $result $this->checkEmptyResultNotMasseur($result$citySpec::orX(ProfileIsLocated::nearStations($city$station->getDistrict()->getStations()->toArray())), self::RESULT_SOURCE_STATION);
  292.         } else {
  293.             $result $this->checkCityAndCountrySource($result$city);
  294.         }
  295.         if ($result->count() > $prevCount) {
  296.             $response?->setMaxAge(60);
  297.         }
  298.         return $this->render('ProfileList/list.html.twig', [
  299.             'profiles' => $result,
  300.             'source' => $this->source,
  301.             'source_default' => self::RESULT_SOURCE_STATION,
  302.             'station' => $station,
  303.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  304.                 'city' => $city->getUriIdentity(),
  305.                 'station' => $station->getUriIdentity(),
  306.                 'page' => $this->getCurrentPageNumber()
  307.             ]),
  308.             'recommendationSpec' => $specs->recommendationSpec(),
  309.         ], response$response);
  310.     }
  311.     private function addSinglePageStationResults(Page $resultCity $cityStation $stationint $spread): Page
  312.     {
  313.         if ($result->totalCount() >= $result->getCurrentLimit()) {
  314.             return $result;
  315.         }
  316.         $addedProfileIds $this->stationAddedProfilesCache->get(self::CACHE_ITEM_STATION_ADDED_PROFILES $station->getId(), function () use ($result$city$station$spread): array {
  317.             $currentSpread rand(0$spread);
  318.             $plannedTotalCount $result->getCurrentLimit() - $spread $currentSpread;
  319.             $result iterator_to_array($result->getIterator());
  320.             $originalProfileIds $this->extractProfileIds($result);
  321.             if ($station->getDistrict()) {
  322.                 $result $this->addSinglePageResultsUptoAmount($result$citySpec::orX(ProfileIsLocated::withinDistrict($station->getDistrict())), $plannedTotalCount);
  323.             }
  324.             if ($station->getDistrict()?->getCounty()) {
  325.                 $result $this->addSinglePageResultsUptoAmount($result$citySpec::orX(ProfileIsLocated::withinCounty($station->getDistrict()->getCounty())), $plannedTotalCount);
  326.             }
  327.             $result $this->addSinglePageResultsUptoAmount($result$citySpec::orX(ProfileIsLocated::withinCity($city)), $plannedTotalCount);
  328.             $result $this->extractProfileIds($result);
  329.             return array_diff($result$originalProfileIds);
  330.         });
  331.         $addedProfileIds array_slice($addedProfileIds0$result->getCurrentLimit() - $result->totalCount());
  332.         $originalProfiles iterator_to_array($result->getIterator());
  333.         $addedProfiles $this->listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacementLimited($city, new ProfileIdIn($addedProfileIds), null, [Genders::FEMALE], count($addedProfileIds));
  334.         $newResult array_merge($originalProfiles$addedProfiles);
  335.         return new FakeORMQueryPage(01$result->getCurrentLimit(), count($newResult), $newResult);
  336.     }
  337.     private function addSinglePageResultsUptoAmount(array $resultCity $city, ?Filter $specsint $totalCount): array
  338.     {
  339.         $toAdd $totalCount count($result);
  340.         $currentResultIds $this->extractProfileIds($result);
  341.         $resultsToAdd $this->listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacementLimited($city$specs, [new ProfileIdNotIn($currentResultIds)], [Genders::FEMALE], $toAdd);
  342.         $result array_merge($result$resultsToAdd);
  343.         return $result;
  344.     }
  345.     #[ParamConverter("city"converter"city_converter")]
  346.     public function listByStations(City $citystring $stationsStationRepository $stationRepository): Response
  347.     {
  348.         $stationIds explode(','$stations);
  349.         $stations $stationRepository->findBy(['uriIdentity' => $stationIds]);
  350.         $specs $this->profileListSpecificationService->listByStations($stations);
  351.         $response = new Response();
  352.         $result $this->paginatedListing($citynull, [], $specs->spec(), nullnull$response);
  353.         return $this->render('ProfileList/list.html.twig', [
  354.             'profiles' => $result,
  355.             'recommendationSpec' => $specs->recommendationSpec(),
  356.         ]);
  357.     }
  358.     #[ParamConverter("city"converter"city_converter")]
  359.     public function listApproved(Request $requestCity $city): Response
  360.     {
  361.         $specs $this->profileListSpecificationService->listApproved();
  362.         $response = new Response();
  363.         $result $this->paginatedListing($city'/city/{city}/approved', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  364.         $prevCount $result->count();
  365.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  366.             $this->source self::RESULT_SOURCE_WITH_COMMENTS;
  367.             $result $this->listRandomSinglePage($citynull, new ProfileHasComments(), nulltruefalse);
  368.             if ($result->count() == 0) {
  369.                 $this->source self::RESULT_SOURCE_WITH_VIDEO;
  370.                 $result $this->listRandomSinglePage($citynull, new ProfileHasVideo(), nulltruefalse);
  371.             }
  372.             if ($result->count() == 0) {
  373.                 $this->source self::RESULT_SOURCE_ELITE;
  374.                 $result $this->listRandomSinglePage($citynull$this->getSpecForEliteGirls($city), nulltruenull);
  375.             }
  376.             $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  377.         }
  378.         if ($result->count() > $prevCount) {
  379.             $response?->setMaxAge(60);
  380.         }
  381.         return $this->render('ProfileList/list.html.twig', [
  382.             'profiles' => $result,
  383.             'source' => $this->source,
  384.             'source_default' => self::RESULT_SOURCE_APPROVED,
  385.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  386.                 'city' => $city->getUriIdentity(),
  387.                 'page' => $this->getCurrentPageNumber()
  388.             ]),
  389.             'recommendationSpec' => $specs->recommendationSpec(),
  390.         ], response$response);
  391.     }
  392.     #[ParamConverter("city"converter"city_converter")]
  393.     public function listWithComments(Request $requestCity $city): Response
  394.     {
  395.         $specs $this->profileListSpecificationService->listWithComments();
  396.         $response = new Response();
  397.         $result $this->paginatedListing($city'/city/{city}/with_comments', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  398.         $prevCount $result->count();
  399.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  400.             $this->source self::RESULT_SOURCE_APPROVED;
  401.             $result $this->listRandomSinglePage($citynull, new ProfileIsApproved(), nulltruefalse);
  402.             if ($result->count() == 0) {
  403.                 $this->source self::RESULT_SOURCE_WITH_VIDEO;
  404.                 $result $this->listRandomSinglePage($citynull, new ProfileHasVideo(), nulltruefalse);
  405.             }
  406.             if ($result->count() == 0) {
  407.                 $this->source self::RESULT_SOURCE_ELITE;
  408.                 $result $this->listRandomSinglePage($citynull$this->getSpecForEliteGirls($city), nulltruenull);
  409.             }
  410.             $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  411.         }
  412.         if ($result->count() > $prevCount) {
  413.             $response?->setMaxAge(60);
  414.         }
  415.         return $this->render('ProfileList/list.html.twig', [
  416.             'profiles' => $result,
  417.             'source' => $this->source,
  418.             'source_default' => self::RESULT_SOURCE_WITH_COMMENTS,
  419.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  420.                 'city' => $city->getUriIdentity(),
  421.                 'page' => $this->getCurrentPageNumber()
  422.             ]),
  423.             'recommendationSpec' => $specs->recommendationSpec(),
  424.         ], response$response);
  425.     }
  426.     #[ParamConverter("city"converter"city_converter")]
  427.     public function listWithVideo(Request $requestCity $city): Response
  428.     {
  429.         $specs $this->profileListSpecificationService->listWithVideo();
  430.         $response = new Response();
  431.         $result $this->paginatedListing($city'/city/{city}/with_video', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  432.         $prevCount $result->count();
  433.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  434.             $this->source self::RESULT_SOURCE_APPROVED;
  435.             $result $this->listRandomSinglePage($citynull, new ProfileIsApproved(), nulltruefalse);
  436.             if ($result->count() == 0) {
  437.                 $this->source self::RESULT_SOURCE_WITH_COMMENTS;
  438.                 $result $this->listRandomSinglePage($citynull, new ProfileHasComments(), nulltruefalse);
  439.             }
  440.             if ($result->count() == 0) {
  441.                 $this->source self::RESULT_SOURCE_ELITE;
  442.                 $result $this->listRandomSinglePage($citynull$this->getSpecForEliteGirls($city), nulltruenull);
  443.             }
  444.             $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  445.         }
  446.         if ($result->count() > $prevCount) {
  447.             $response?->setMaxAge(60);
  448.         }
  449.         return $this->render('ProfileList/list.html.twig', [
  450.             'profiles' => $result,
  451.             'source' => $this->source,
  452.             'source_default' => self::RESULT_SOURCE_WITH_VIDEO,
  453.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  454.                 'city' => $city->getUriIdentity(),
  455.                 'page' => $this->getCurrentPageNumber()
  456.             ]),
  457.             'recommendationSpec' => $specs->recommendationSpec(),
  458.         ], response$response);
  459.     }
  460.     #[ParamConverter("city"converter"city_converter")]
  461.     public function listWithSelfie(Request $requestCity $city): Response
  462.     {
  463.         $specs $this->profileListSpecificationService->listWithSelfie();
  464.         $response = new Response();
  465.         $result $this->paginatedListing($city'/city/{city}/with_selfie', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  466.         $prevCount $result->count();
  467.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  468.             $this->source self::RESULT_SOURCE_WITH_VIDEO;
  469.             $result $this->listRandomSinglePage($citynull, new ProfileHasVideo(), nulltruefalse);
  470.             if ($result->count() == 0) {
  471.                 $this->source self::RESULT_SOURCE_APPROVED;
  472.                 $result $this->listRandomSinglePage($citynull, new ProfileIsApproved(), nulltruefalse);
  473.             }
  474.             if ($result->count() == 0) {
  475.                 $this->source self::RESULT_SOURCE_ELITE;
  476.                 $result $this->listRandomSinglePage($citynull$this->getSpecForEliteGirls($city), nulltruenull);
  477.             }
  478.             $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  479.         }
  480.         if ($result->count() > $prevCount) {
  481.             $response?->setMaxAge(60);
  482.         }
  483.         return $this->render('ProfileList/list.html.twig', [
  484.             'profiles' => $result,
  485.             'source' => $this->source,
  486.             'source_default' => self::RESULT_SOURCE_WITH_SELFIE,
  487.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  488.                 'city' => $city->getUriIdentity(),
  489.                 'page' => $this->getCurrentPageNumber()
  490.             ]),
  491.             'recommendationSpec' => $specs->recommendationSpec(),
  492.         ], response$response);
  493.     }
  494.     #[ParamConverter("city"converter"city_converter")]
  495.     public function listByPrice(Request $requestCountryCurrencyResolver $countryCurrencyResolverCity $citystring $priceTypeint $minPrice nullint $maxPrice null): Response
  496.     {
  497.         $specs $this->profileListSpecificationService->listByPrice($city$priceType$minPrice$maxPrice);
  498.         $response = new Response();
  499.         $apiEndpoint in_array($priceType, ['low''high''elite']) ? '/city/{city}/price/'.$priceType null;
  500.         $result $this->paginatedListing($city$apiEndpoint, ['city' => $city->getId()], $specs->spec(), nullnull$response);
  501.         $prevCount $result->count();
  502.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  503.             $result $this->processListByPriceEmptyResult($result$city$priceType$minPrice$maxPrice);
  504.         }
  505.         if ($result->count() > $prevCount) {
  506.             $response?->setMaxAge(60);
  507.         }
  508.         return $this->render('ProfileList/list.html.twig', [
  509.             'profiles' => $result,
  510.             'source' => $this->source,
  511.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  512.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  513.                 'city' => $city->getUriIdentity(),
  514.                 'priceType' => $priceType,
  515.                 'minPrice' => $minPrice,
  516.                 'maxPrice' => $maxPrice,
  517.                 'page' => $this->getCurrentPageNumber()
  518.             ]),
  519.             'recommendationSpec' => $specs->recommendationSpec(),
  520.         ], response$response);
  521.     }
  522.     private function processListByPriceEmptyResult(Page $resultCity $citystring $priceTypeint $minPrice nullint $maxPrice null)
  523.     {
  524.         if (!$this->features->fill_empty_profile_list())
  525.             return $result;
  526.         $this->source self::RESULT_SOURCE_BY_PARAMS;
  527.         if ($this->countryCurrencyResolver->getCurrencyFor($city->getCountryCode()) == 'RUB') {
  528.             if ($minPrice && $maxPrice) {
  529.                 if ($minPrice == 2000 && $maxPrice == 3000) {
  530.                     $priceSpec = [
  531.                         ProfileWithApartmentsOneHourPrice::range(15002000),
  532.                         ProfileWithApartmentsOneHourPrice::range(30004000),
  533.                     ];
  534.                 } else if ($minPrice == 3000 && $maxPrice == 4000) {
  535.                     $priceSpec = [
  536.                         ProfileWithApartmentsOneHourPrice::range(20003000),
  537.                         ProfileWithApartmentsOneHourPrice::range(40005000),
  538.                     ];
  539.                 } else if ($minPrice == 4000 && $maxPrice == 5000) {
  540.                     $priceSpec = [
  541.                         ProfileWithApartmentsOneHourPrice::range(30004000),
  542.                         ProfileWithApartmentsOneHourPrice::range(50006000),
  543.                     ];
  544.                 } else if ($minPrice == 5000 && $maxPrice == 6000) {
  545.                     $priceSpec = [
  546.                         ProfileWithApartmentsOneHourPrice::range(4000999999)
  547.                     ];
  548.                 } else {
  549.                     $priceSpec = [
  550.                         ProfileWithApartmentsOneHourPrice::range($minPrice$maxPrice)
  551.                     ];
  552.                 }
  553.                 $result $this->listRandomSinglePage($citynullnull$priceSpectruefalse);
  554.             } elseif ($maxPrice) {
  555.                 if ($maxPrice == 500) {
  556.                     $priceSpec ProfileWithApartmentsOneHourPrice::cheaperThan(1500);
  557.                     $result $this->listRandomSinglePage($citynull$priceSpecnulltruefalse);
  558.                     if ($result->count() == 0) {
  559.                         $priceSpec ProfileWithApartmentsOneHourPrice::range(15002000);
  560.                         $result $this->listRandomSinglePage($citynull$priceSpecnulltruefalse);
  561.                     }
  562.                 } else if ($maxPrice == 1500) {
  563.                     $priceSpec ProfileWithApartmentsOneHourPrice::range(15002000);
  564.                     $result $this->listRandomSinglePage($citynull$priceSpecnulltruefalse);
  565.                     if ($result->count() == 0) {
  566.                         $priceSpec ProfileWithApartmentsOneHourPrice::range(20003000);
  567.                         $result $this->listRandomSinglePage($citynull$priceSpecnulltruefalse);
  568.                     }
  569.                 }
  570.             } else {
  571.                 switch ($priceType) {
  572.                     case 'not_expensive':
  573.                         $priceSpec ProfileWithApartmentsOneHourPrice::cheaperThan(2000);
  574.                         break;
  575.                     case 'high':
  576.                         $priceSpec ProfileWithApartmentsOneHourPrice::range(30006000);
  577.                         break;
  578.                     case 'low':
  579.                         $priceSpec ProfileWithApartmentsOneHourPrice::cheaperThan(2000);
  580.                         break;
  581.                     case 'elite':
  582.                         $priceSpec ProfileWithApartmentsOneHourPrice::moreExpensiveThan(6000);
  583.                         break;
  584.                     default:
  585.                         throw new \LogicException('Unknown price type');
  586.                         break;
  587.                 }
  588.                 $result $this->listRandomSinglePage($citynull$priceSpecnulltruefalse);
  589.             }
  590.         }
  591.         $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  592.         return $result;
  593.     }
  594.     #[ParamConverter("city"converter"city_converter")]
  595.     public function listByAge(Request $requestCity $citystring $ageTypeint $minAge nullint $maxAge null): Response
  596.     {
  597.         $specs $this->profileListSpecificationService->listByAge($ageType$minAge$maxAge);
  598.         $response = new Response();
  599.         $apiEndpoint in_array($ageType, ['young''old']) ? '/city/{city}/age/'.$ageType null;
  600.         $result $this->paginatedListing($city$apiEndpoint, ['city' => $city->getId()], $specs->spec(), nullnull$response);
  601.         $prevCount $result->count();
  602.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  603.             $filled $this->processListByAgeEmptyResult($result$city$ageType$minAge$maxAge);
  604.             if ($filled)
  605.                 $result $filled;
  606.         }
  607.         if ($result->count() > $prevCount) {
  608.             $response?->setMaxAge(60);
  609.         }
  610.         return $this->render('ProfileList/list.html.twig', [
  611.             'profiles' => $result,
  612.             'source' => $this->source,
  613.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  614.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  615.                 'city' => $city->getUriIdentity(),
  616.                 'ageType' => $ageType,
  617.                 'minAge' => $minAge,
  618.                 'maxAge' => $maxAge,
  619.                 'page' => $this->getCurrentPageNumber()
  620.             ]),
  621.             'recommendationSpec' => $specs->recommendationSpec(),
  622.         ], response$response);
  623.     }
  624.     private function processListByAgeEmptyResult(Page $resultCity $citystring $ageTypeint $minAge nullint $maxAge null)
  625.     {
  626.         if (!$this->features->fill_empty_profile_list())
  627.             return $result;
  628.         $this->source self::RESULT_SOURCE_BY_PARAMS;
  629.         if ($minAge && !$maxAge) {
  630.             $startMinAge $minAge;
  631.             do {
  632.                 $startMinAge -= 2;
  633.                 $ageSpec ProfileWithAge::olderThan($startMinAge);
  634.                 $result $this->listRandomSinglePage($citynull$ageSpecnulltruefalse);
  635.             } while ($result->count() == && $startMinAge >= 18);
  636.         } else if ($ageType == 'young') {
  637.             $startMaxAge 20;
  638.             do {
  639.                 $startMaxAge += 2;
  640.                 $ageSpec ProfileWithAge::youngerThan($startMaxAge);
  641.                 $result $this->listRandomSinglePage($citynull$ageSpecnulltruefalse);
  642.             } while ($result->count() == && $startMaxAge <= 100);
  643.         }
  644.         $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  645.         return $result;
  646.     }
  647.     #[ParamConverter("city"converter"city_converter")]
  648.     public function listByHeight(Request $requestCity $citystring $heightType): Response
  649.     {
  650.         $specs $this->profileListSpecificationService->listByHeight($heightType);
  651.         $response = new Response();
  652.         $result $this->paginatedListing($city'/city/{city}/height/'.$heightType, ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  653.         return $this->render('ProfileList/list.html.twig', [
  654.             'profiles' => $result,
  655.             'source' => $this->source,
  656.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  657.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  658.                 'city' => $city->getUriIdentity(),
  659.                 'heightType' => $heightType,
  660.                 'page' => $this->getCurrentPageNumber()
  661.             ]),
  662.             'recommendationSpec' => $specs->recommendationSpec(),
  663.         ], response$response);
  664.     }
  665.     #[ParamConverter("city"converter"city_converter")]
  666.     public function listByBreastType(Request $requestCity $citystring $breastType): Response
  667.     {
  668.         if (null === $type BreastTypes::getValueByUriIdentity($breastType)) {
  669.             throw $this->createNotFoundException();
  670.         }
  671.         $specs $this->profileListSpecificationService->listByBreastType($breastType);
  672.         $response = new Response();
  673.         $alternativeSpec $this->getORSpecForItemsArray(BreastTypes::getList(), function($item): ProfileWithBreastType {
  674.             return new ProfileWithBreastType($item);
  675.         });
  676.         $result $this->paginatedListing($city'/city/{city}/breasttype/'.$type, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  677.         return $this->render('ProfileList/list.html.twig', [
  678.             'profiles' => $result,
  679.             'source' => $this->source,
  680.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  681.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  682.                 'city' => $city->getUriIdentity(),
  683.                 'breastType' => $breastType,
  684.                 'page' => $this->getCurrentPageNumber()
  685.             ]),
  686.             'recommendationSpec' => $specs->recommendationSpec(),
  687.         ], response$response);
  688.     }
  689.     #[ParamConverter("city"converter"city_converter")]
  690.     public function listByHairColor(Request $requestCity $citystring $hairColor): Response
  691.     {
  692.         if (null === $color HairColors::getValueByUriIdentity($hairColor)) {
  693.             throw $this->createNotFoundException();
  694.         }
  695.         $specs $this->profileListSpecificationService->listByHairColor($hairColor);
  696.         $response = new Response();
  697.         $alternativeSpec $this->getORSpecForItemsArray(HairColors::getList(), function($item): ProfileWithHairColor {
  698.             return new ProfileWithHairColor($item);
  699.         });
  700.         $result $this->paginatedListing($city'/city/{city}/haircolor/'.$color, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  701.         return $this->render('ProfileList/list.html.twig', [
  702.             'profiles' => $result,
  703.             'source' => $this->source,
  704.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  705.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  706.                 'city' => $city->getUriIdentity(),
  707.                 'hairColor' => $hairColor,
  708.                 'page' => $this->getCurrentPageNumber()
  709.             ]),
  710.             'recommendationSpec' => $specs->recommendationSpec(),
  711.         ], response$response);
  712.     }
  713.     #[ParamConverter("city"converter"city_converter")]
  714.     public function listByBodyType(Request $requestCity $citystring $bodyType): Response
  715.     {
  716.         if (null === $type BodyTypes::getValueByUriIdentity($bodyType)) {
  717.             throw $this->createNotFoundException();
  718.         }
  719.         $specs $this->profileListSpecificationService->listByBodyType($bodyType);
  720.         $response = new Response();
  721.         $alternativeSpec $this->getORSpecForItemsArray(BodyTypes::getList(), function($item): ProfileWithBodyType {
  722.             return new ProfileWithBodyType($item);
  723.         });
  724.         $result $this->paginatedListing($city'/city/{city}/bodytype/'.$type, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  725.         return $this->render('ProfileList/list.html.twig', [
  726.             'profiles' => $result,
  727.             'source' => $this->source,
  728.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  729.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  730.                 'city' => $city->getUriIdentity(),
  731.                 'bodyType' => $bodyType,
  732.                 'page' => $this->getCurrentPageNumber()
  733.             ]),
  734.             'recommendationSpec' => $specs->recommendationSpec(),
  735.         ], response$response);
  736.     }
  737.     #[ParamConverter("city"converter"city_converter")]
  738.     public function listByPlace(Request $requestCity $citystring $placeTypestring $takeOutLocation null): Response
  739.     {
  740.         $specs $this->profileListSpecificationService->listByPlace($placeType$takeOutLocation);
  741.         if (null === $specs) {
  742.             throw $this->createNotFoundException();
  743.         }
  744.         $response = new Response();
  745.         $alternativeSpec $this->getORSpecForItemsArray(TakeOutLocations::getList(), function($item): ProfileIsProvidingTakeOut {
  746.             return new ProfileIsProvidingTakeOut($item);
  747.         });
  748.         if ($placeType === 'take-out') {
  749.             $alternativeSpec->orX(new ProfileHasApartments());
  750.         }
  751.         $apiEndpoint '/city/{city}/place/'.$placeType;
  752.         if (null !== $takeOutLocation) {
  753.             $apiEndpoint .= '/'.$takeOutLocation;
  754.         }
  755.         $result $this->paginatedListing($city$apiEndpoint, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  756.         return $this->render('ProfileList/list.html.twig', [
  757.             'profiles' => $result,
  758.             'source' => $this->source,
  759.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  760.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  761.                 'city' => $city->getUriIdentity(),
  762.                 'placeType' => $placeType,
  763.                 'takeOutLocation' => TakeOutLocations::getUriIdentity(TakeOutLocations::getValueByUriIdentity($takeOutLocation)),
  764.                 'page' => $this->getCurrentPageNumber()
  765.             ]),
  766.             'recommendationSpec' => $specs->recommendationSpec(),
  767.         ], response$response);
  768.     }
  769.     #[ParamConverter("city"converter"city_converter")]
  770.     public function listByPrivateHaircut(Request $requestCity $citystring $privateHaircut): Response
  771.     {
  772.         if(null === $type PrivateHaircuts::getValueByUriIdentity($privateHaircut))
  773.             throw $this->createNotFoundException();
  774.         $specs $this->profileListSpecificationService->listByPrivateHaircut($privateHaircut);
  775.         $response = new Response();
  776.         $apiEndpoint '/city/{city}/privatehaircut/'.$type;
  777.         $alternativeSpec $this->getORSpecForItemsArray(PrivateHaircuts::getList(), function($item): ProfileWithPrivateHaircut {
  778.             return new ProfileWithPrivateHaircut($item);
  779.         });
  780.         $result $this->paginatedListing($city$apiEndpoint, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  781.         return $this->render('ProfileList/list.html.twig', [
  782.             'profiles' => $result,
  783.             'source' => $this->source,
  784.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  785.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  786.                 'city' => $city->getUriIdentity(),
  787.                 'privateHaircut' => $privateHaircut,
  788.                 'page' => $this->getCurrentPageNumber()
  789.             ]),
  790.             'recommendationSpec' => $specs->recommendationSpec(),
  791.         ], response$response);
  792.     }
  793.     #[ParamConverter("city"converter"city_converter")]
  794.     public function listByNationality(Request $requestCity $citystring $nationality): Response
  795.     {
  796.         if(null === $type Nationalities::getValueByUriIdentity($nationality))
  797.             throw $this->createNotFoundException();
  798.         $specs $this->profileListSpecificationService->listByNationality($nationality);
  799.         $response = new Response();
  800.         $alternativeSpec $this->getORSpecForItemsArray(Nationalities::getList(), function($item): ProfileWithNationality {
  801.             return new ProfileWithNationality($item);
  802.         });
  803.         $apiEndpoint '/city/{city}/nationality/'.$type;
  804.         $result $this->paginatedListing($city$apiEndpoint, ['city' => $city->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_BY_PARAMS$response);
  805.         return $this->render('ProfileList/list.html.twig', [
  806.             'profiles' => $result,
  807.             'source' => $this->source,
  808.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  809.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  810.                 'city' => $city->getUriIdentity(),
  811.                 'nationality' => $nationality,
  812.                 'page' => $this->getCurrentPageNumber()
  813.             ]),
  814.             'recommendationSpec' => $specs->recommendationSpec(),
  815.         ], response$response);
  816.     }
  817.     #[ParamConverter("city"converter"city_converter")]
  818.     #[ParamConverter("service"options: ['mapping' => ['service' => 'uriIdentity']])]
  819.     public function listByProvidedService(Request $requestCity $cityService $service): Response
  820.     {
  821.         $specs $this->profileListSpecificationService->listByProvidedService($service$city);
  822.         $response = new Response();
  823.         $sameGroupServices $this->serviceRepository->findBy(['group' => $service->getGroup()]);
  824.         $alternativeSpec $this->getORSpecForItemsArray([$sameGroupServices], function($item): ProfileIsProvidingOneOfServices {
  825.             return new ProfileIsProvidingOneOfServices($item);
  826.         });
  827.         $result $this->paginatedListing($city'/city/{city}/service/{service}', ['city' => $city->getId(), 'service' => $service->getId()], $specs->spec(), $alternativeSpecself::RESULT_SOURCE_SERVICE$response);
  828.         return $this->render('ProfileList/list.html.twig', [
  829.             'profiles' => $result,
  830.             'source' => $this->source,
  831.             'source_default' => self::RESULT_SOURCE_SERVICE,
  832.             'service' => $service,
  833.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  834.                 'city' => $city->getUriIdentity(),
  835.                 'service' => $service->getUriIdentity(),
  836.                 'page' => $this->getCurrentPageNumber()
  837.             ]),
  838.             'recommendationSpec' => $specs->recommendationSpec(),
  839.         ], response$response);
  840.     }
  841.     /**
  842.      * @Feature("has_archive_page")
  843.      */
  844.     #[ParamConverter("city"converter"city_converter")]
  845.     public function listArchived(Request $requestCity $city): Response
  846.     {
  847.         $result $this->profileList->list($citynullnullnullfalsenullProfileList::ORDER_BY_UPDATED);
  848.         return $this->render('ProfileList/list.html.twig', [
  849.             'profiles' => $result,
  850.             'recommendationSpec' => new \App\Specification\ElasticSearch\ProfileIsNotArchived(), //ProfileIsArchived, согласно https://redminez.net/issues/28305 в реках выводятся неарзивные
  851.         ]);
  852.     }
  853.     #[ParamConverter("city"converter"city_converter")]
  854.     public function listNew(City $cityint $weeks 2): Response
  855.     {
  856.         $specs $this->profileListSpecificationService->listNew($weeks);
  857.         $response = new Response();
  858.         $result $this->paginatedListing($city'/city/{city}/recent', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  859.         return $this->render('ProfileList/list.html.twig', [
  860.             'profiles' => $result,
  861.             'recommendationSpec' => $specs->recommendationSpec(),
  862.         ], response$response);
  863.     }
  864.     #[ParamConverter("city"converter"city_converter")]
  865.     public function listByNoRetouch(City $city): Response
  866.     {
  867.         $specs $this->profileListSpecificationService->listByNoRetouch();
  868.         $response = new Response();
  869.         $result $this->paginatedListing($city'/city/{city}/noretouch', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  870.         return $this->render('ProfileList/list.html.twig', [
  871.             'profiles' => $result,
  872.             'source' => $this->source,
  873.             'recommendationSpec' => $specs->recommendationSpec(),
  874.         ], response$response);
  875.     }
  876.     #[ParamConverter("city"converter"city_converter")]
  877.     public function listByNice(City $city): Response
  878.     {
  879.         $specs $this->profileListSpecificationService->listByNice();
  880.         $response = new Response();
  881.         $result $this->paginatedListing($city'/city/{city}/nice', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  882.         return $this->render('ProfileList/list.html.twig', [
  883.             'profiles' => $result,
  884.             'source' => $this->source,
  885.             'recommendationSpec' => $specs->recommendationSpec(),
  886.         ], response$response);
  887.     }
  888.     #[ParamConverter("city"converter"city_converter")]
  889.     public function listByOnCall(City $city): Response
  890.     {
  891.         $specs $this->profileListSpecificationService->listByOnCall();
  892.         $response = new Response();
  893.         $result $this->paginatedListing($city'/city/{city}/oncall', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  894.         return $this->render('ProfileList/list.html.twig', [
  895.             'profiles' => $result,
  896.             'source' => $this->source,
  897.             'recommendationSpec' => $specs->recommendationSpec(),
  898.         ], response$response);
  899.     }
  900.     #[ParamConverter("city"converter"city_converter")]
  901.     public function listForHour(City $city): Response
  902.     {
  903.         $specs $this->profileListSpecificationService->listForHour();
  904.         $response = new Response();
  905.         $result $this->paginatedListing($city'/city/{city}/forhour', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  906.         return $this->render('ProfileList/list.html.twig', [
  907.             'profiles' => $result,
  908.             'source' => $this->source,
  909.             'recommendationSpec' => $specs->recommendationSpec(),
  910.         ], response$response);
  911.     }
  912.     #[ParamConverter("city"converter"city_converter")]
  913.     public function listForNight(City $city): Response
  914.     {
  915.         $specs $this->profileListSpecificationService->listForNight();
  916.         $response = new Response();
  917.         $result $this->paginatedListing($city'/city/{city}/fornight', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  918.         return $this->render('ProfileList/list.html.twig', [
  919.             'profiles' => $result,
  920.             'source' => $this->source,
  921.             'recommendationSpec' => $specs->recommendationSpec(),
  922.         ], response$response);
  923.     }
  924.     private function getSpecForEliteGirls(City $city): Filter
  925.     {
  926.         $minPrice $this->countryCurrencyResolver->getValueByCountryCode($city->getCountryCode(), [
  927.             'RUB' => 5000,
  928.             'UAH' => 1500,
  929.             'USD' => 100,
  930.             'EUR' => 130,
  931.         ]);
  932.         return new ProfileIsElite($minPrice);
  933.     }
  934.     private function getElasticSearchSpecForEliteGirls(City $city): ISpecification
  935.     {
  936.         $minPrice $this->countryCurrencyResolver->getValueByCountryCode($city->getCountryCode(), [
  937.             'RUB' => 5000,
  938.             'UAH' => 1500,
  939.             'USD' => 100,
  940.             'EUR' => 130,
  941.         ]);
  942.         return new \App\Specification\ElasticSearch\ProfileIsElite($minPrice);
  943.     }
  944.     #[ParamConverter("city"converter"city_converter")]
  945.     public function listForEliteGirls(CountryCurrencyResolver $countryCurrencyResolverRequest $requestCity $city): Response
  946.     {
  947.         $specs $this->profileListSpecificationService->listForEliteGirls($city);
  948.         $response = new Response();
  949.         $result $this->paginatedListing($city'/city/{city}/elite', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  950.         $prevCount $result->count();
  951.         if ($this->features->fill_empty_profile_list() && $result->count() == 0) {
  952.             $prices = [
  953.                 'RUB' => 5000,
  954.                 'UAH' => 1500,
  955.                 'USD' => 100,
  956.                 'EUR' => 130,
  957.             ];
  958.             $currency $countryCurrencyResolver->getCurrencyFor($city->getCountryCode());
  959.             if (isset($prices[$currency])) {
  960.                 $minPrice $prices[$currency];
  961.                 switch ($currency) {
  962.                     case 'RUB':
  963.                         $diff 1000;
  964.                         break;
  965.                     case 'UAH':
  966.                         $diff 500;
  967.                         break;
  968.                     case 'USD':
  969.                     case 'EUR':
  970.                         $diff 20;
  971.                         break;
  972.                     default:
  973.                         throw new \LogicException('Unexpected currency code');
  974.                 }
  975.                 while ($minPrice >= $diff) {
  976.                     $minPrice -= $diff;
  977.                     $result $this->listRandomSinglePage($citynullProfileWithApartmentsOneHourPrice::moreExpensiveThan($minPrice), nulltruefalse);
  978.                     if ($result->count() > 0) {
  979.                         $this->source self::RESULT_SOURCE_BY_PARAMS;
  980.                         break;
  981.                     }
  982.                 }
  983.                 $result $this->checkEmptyResultNotMasseur($result$citynullself::RESULT_SOURCE_CITY);
  984.             }
  985.         }
  986.         if ($result->count() > $prevCount) {
  987.             $response?->setMaxAge(60);
  988.         }
  989.         return $this->render('ProfileList/list.html.twig', [
  990.             'profiles' => $result,
  991.             'source' => $this->source,
  992.             'source_default' => self::RESULT_SOURCE_BY_PARAMS,
  993.             'category_url' => $this->generateUrl($request->attributes->get('_route'), [
  994.                 'city' => $city->getUriIdentity(),
  995.                 'page' => $this->getCurrentPageNumber()
  996.             ]),
  997.             'recommendationSpec' => $specs->recommendationSpec(),
  998.         ], response$response);
  999.     }
  1000.     #[ParamConverter("city"converter"city_converter")]
  1001.     public function listForRealElite(CountryCurrencyResolver $countryCurrencyResolverCity $city): Response
  1002.     {
  1003.         $specs $this->profileListSpecificationService->listForRealElite($city);
  1004.         $response = new Response();
  1005.         $result $this->paginatedListing($city'/city/{city}/realelite', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1006.         return $this->render('ProfileList/list.html.twig', [
  1007.             'profiles' => $result,
  1008.             'source' => $this->source,
  1009.             'recommendationSpec' => $specs->recommendationSpec(),
  1010.         ], response$response);
  1011.     }
  1012.     #[ParamConverter("city"converter"city_converter")]
  1013.     public function listForVipPros(CountryCurrencyResolver $countryCurrencyResolverCity $city): Response
  1014.     {
  1015.         $specs $this->profileListSpecificationService->listForVipPros($city);
  1016.         $response = new Response();
  1017.         $result $this->paginatedListing($city'/city/{city}/vip', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1018.         return $this->render('ProfileList/list.html.twig', [
  1019.             'profiles' => $result,
  1020.             'source' => $this->source,
  1021.             'recommendationSpec' => $specs->recommendationSpec(),
  1022.         ], response$response);
  1023.     }
  1024.     #[ParamConverter("city"converter"city_converter")]
  1025.     public function listForVipIndividual(CountryCurrencyResolver $countryCurrencyResolverCity $city): Response
  1026.     {
  1027.         $specs $this->profileListSpecificationService->listForVipIndividual($city);
  1028.         $response = new Response();
  1029.         $result $this->paginatedListing($city'/city/{city}/vipindi', ['city' => $city->getId()], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1030.         return $this->render('ProfileList/list.html.twig', [
  1031.             'profiles' => $result,
  1032.             'source' => $this->source,
  1033.             'recommendationSpec' => $specs->recommendationSpec(),
  1034.         ], response$response);
  1035.     }
  1036.     #[ParamConverter("city"converter"city_converter")]
  1037.     public function listForVipGirlsCity(City $city): Response
  1038.     {
  1039.         $specs $this->profileListSpecificationService->listForVipGirlsCity($city);
  1040.         $response = new Response();
  1041.         $result $this->paginatedListing($citynull, [], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1042.         return $this->render('ProfileList/list.html.twig', [
  1043.             'profiles' => $result,
  1044.             'source' => $this->source,
  1045.             'recommendationSpec' => $specs->recommendationSpec(),
  1046.         ], response$response);
  1047.     }
  1048.     #[ParamConverter("city"converter"city_converter")]
  1049.     public function listOfGirlfriends(City $city): Response
  1050.     {
  1051.         $specs $this->profileListSpecificationService->listOfGirlfriends();
  1052.         $response = new Response();
  1053.         $result $this->paginatedListing($citynull, [], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1054.         return $this->render('ProfileList/list.html.twig', [
  1055.             'profiles' => $result,
  1056.             'source' => $this->source,
  1057.             'recommendationSpec' => $specs->recommendationSpec(),
  1058.         ]);
  1059.     }
  1060.     #[ParamConverter("city"converter"city_converter")]
  1061.     public function listOfMostExpensive(City $city): Response
  1062.     {
  1063.         $specs $this->profileListSpecificationService->listOfMostExpensive($city);
  1064.         $response = new Response();
  1065.         $result $this->paginatedListing($citynull, [], $specs->spec(), nullself::RESULT_SOURCE_CITY$response);
  1066.         return $this->render('ProfileList/list.html.twig', [
  1067.             'profiles' => $result,
  1068.             'source' => $this->source,
  1069.             'recommendationSpec' => $specs->recommendationSpec(),
  1070.         ]);
  1071.     }
  1072.     #[ParamConverter("city"converter"city_converter")]
  1073.     public function listBdsm(City $cityServiceRepository $serviceRepositoryParameterBagInterface $parameterBag): Response
  1074.     {
  1075.         $specs $this->profileListSpecificationService->listBdsm();
  1076.         $response = new Response();
  1077.         $result $this->paginatedListing($city'/city/{city}/bdsm', ['city' => $city->getId()], $specs->spec(), nullnull$response);
  1078.         return $this->render('ProfileList/list.html.twig', [
  1079.             'profiles' => $result,
  1080.             'recommendationSpec' => $specs->recommendationSpec(),
  1081.         ], response$response);
  1082.     }
  1083.     #[ParamConverter("city"converter"city_converter")]
  1084.     public function listByGender(City $citystring $genderDefaultCityProvider $defaultCityProvider): Response
  1085.     {
  1086.         if ($city->getId() != $defaultCityProvider->getDefaultCity()->getId()) {
  1087.             throw $this->createNotFoundException();
  1088.         }
  1089.         if (null === Genders::getValueByUriIdentity($gender))
  1090.             throw $this->createNotFoundException();
  1091.         $specs $this->profileListSpecificationService->listByGender($gender);
  1092.         $response = new Response();
  1093.         $result $this->paginatedListing($citynull, [], $specs->spec(), nullnull$response);
  1094.         return $this->render('ProfileList/list.html.twig', [
  1095.             'profiles' => $result,
  1096.             'recommendationSpec' => $specs->recommendationSpec(),
  1097.         ]);
  1098.     }
  1099.     protected function checkCityAndCountrySource(Page $resultCity $city): Page
  1100.     {
  1101.         if (($result && $result->count() != 0) || false == $this->features->fill_empty_profile_list())
  1102.             return $result;
  1103.         $this->source self::RESULT_SOURCE_CITY;
  1104.         $result $this->listRandomSinglePage($citynullnullnulltruefalse);
  1105.         if ($result->count() == 0) {
  1106.             $this->source self::RESULT_SOURCE_COUNTRY;
  1107.             $result $this->listRandomSinglePage($city$city->getCountryCode(), nullnulltruefalse);
  1108.         }
  1109.         return $result;
  1110.     }
  1111.     protected function checkEmptyResultNotMasseur(Page $resultCity $city, ?OrX $alternativeSpecstring $source): Page
  1112.     {
  1113.         if ($result->count() != || false == $this->features->fill_empty_profile_list())
  1114.             return $result;
  1115.         if (null != $alternativeSpec) {
  1116.             $this->source $source;
  1117.             $result $this->listRandomSinglePage($citynull$alternativeSpecnulltruefalse);
  1118.         }
  1119.         if ($result->count() == 0)
  1120.             $result $this->checkCityAndCountrySource($result$city);
  1121.         return $result;
  1122.     }
  1123.     /**
  1124.      * Сейчас не используется, решили доставать их всех соседних подкатегорий разом.
  1125.      * Пока оставил, вдруг передумают.
  1126.      * @deprecated
  1127.      */
  1128.     public function listByNextSimilarCategories(callable $listMethod$requestCategory, array $similarItems): ORMQueryResult
  1129.     {
  1130.         $similarItems array_filter($similarItems, function ($item) use ($requestCategory): bool {
  1131.             return $item != $requestCategory;
  1132.         });
  1133.         //shuffle($similarItems);
  1134.         $item null;
  1135.         $result null;
  1136.         do {
  1137.             $item $item == null current($similarItems) : next($similarItems);
  1138.             if (false === $item)
  1139.                 return $result;
  1140.             $result $listMethod($item);
  1141.         } while ($result->count() == 0);
  1142.         return $result;
  1143.     }
  1144.     protected function getCurrentPageNumber(): int
  1145.     {
  1146.         $page = (int) $this->requestStack->getCurrentRequest()?->get($this->pageParameter1);
  1147.         if ($page 1) {
  1148.             $page 1;
  1149.         }
  1150.         return $page;
  1151.     }
  1152.     protected function render(string $view, array $parameters = [], Response $response null): Response
  1153.     {
  1154.         $this->listingService->setCurrentListingPage($parameters['profiles']);
  1155.         $requestAttrs $this->requestStack->getCurrentRequest();
  1156.         $listing $requestAttrs->get('_controller');
  1157.         $listing is_array($listing) ? $listing[count($listing) - 1] : $listing;
  1158.         $listing preg_replace('/[^:]+::/'''$listing);
  1159.         $listingParameters $requestAttrs->get('_route_params');
  1160.         $listingParameters is_array($listingParameters) ? $listingParameters : [];
  1161.         $mainRequestHasPageParam = isset(($this->requestStack->getMainRequest()->get('_route_params') ?? [])['page']);
  1162.         if ($this->requestStack->getCurrentRequest()->isXmlHttpRequest()) {
  1163.             $view = (
  1164.                 str_starts_with($listing'list')
  1165.                 && 'ProfileList/list.html.twig' === $view
  1166.                 && $mainRequestHasPageParam //isset($listingParameters['page'])
  1167.             )
  1168.                 ? 'ProfileList/list.profiles.html.twig'
  1169.                 $view;
  1170.             return $this->prepareForXhr(parent::render($view$parameters$response));
  1171.             //return $this->getJSONResponse($parameters);
  1172.         } else {
  1173.             $parameters array_merge($parameters, [
  1174.                 'listing' => $listing,
  1175.                 'listing_parameters' => $listingParameters,
  1176.             ]);
  1177.             return parent::render($view$parameters$response);
  1178.         }
  1179.     }
  1180.     private function listActiveWithinCityOrderedByStatusWithSpecAvoidingTopPlacementLimited(
  1181.         City $city,
  1182.         ?Filter $spec,
  1183.         array $additionalSpecs null,
  1184.         array $genders = [Genders::FEMALE],
  1185.         int $limit 0,
  1186.     ): array|Page {
  1187.         return $this->profileList->listActiveWithinCityOrderedByStatusWithSpecLimited($city$spec$additionalSpecs$genderstrue$limit);
  1188.     }
  1189.     private function listRandomSinglePage(
  1190.         City $city,
  1191.         ?string $country,
  1192.         ?Filter $spec,
  1193.         ?array $additionalSpecs,
  1194.         bool $active,
  1195.         ?bool $masseur false,
  1196.         array $genders = [Genders::FEMALE]
  1197.     ): Page {
  1198.         return $this->profileList->listRandom($city$country$spec$additionalSpecs$active$masseur$genderstrue);
  1199.     }
  1200.     private function shuffleProfilesOnPage(Page $result): Page
  1201.     {
  1202.         $profiles iterator_to_array($result->getIterator());
  1203.         if(count($profiles) > 1) {
  1204.             shuffle($profiles);
  1205.         }
  1206.         return new FakeORMQueryPage(
  1207.             $result->getCurrentOffset(),
  1208.             $result->getCurrentPage(),
  1209.             $result->getCurrentLimit(),
  1210.             $result->totalCount(),
  1211.             $profiles
  1212.         );
  1213.     }
  1214.     /**
  1215.      * Достает из списка анкет их id с учетом совместимости разных форматов данных
  1216.      */
  1217.     private function extractProfileIds(array $profiles): array
  1218.     {
  1219.         $ids array_map(static function ($item) {
  1220.             /**
  1221.              * - array - данные из микросервиса ротации через API
  1222.              * - Profile::getId() - полноценная сущность анкеты
  1223.              * - ProfileListingReadModel::$id - read-model анкеты
  1224.              */
  1225.             return is_array($item) ? $item['id'] : ($item?->id ?? $item?->getId());
  1226.         }, $profiles);
  1227.         return array_filter($ids); // remove null values
  1228.     }
  1229. //    protected function getJSONResponse(array $parameters)
  1230. //    {
  1231. //        $request = $this->request;
  1232. //        $data = json_decode($request->getContent(), true);
  1233. //
  1234. //        $imageSize = !empty($data['imageSize']) ? $data['imageSize'] : "357x500";
  1235. //
  1236. //        /** @var FakeORMQueryPage $queryPage */
  1237. //        $queryPage = $parameters['profiles'];
  1238. //
  1239. //        $profiles = array_map(function(ProfileListingReadModel $profile) use ($imageSize) {
  1240. //            $profile->stations = array_values($profile->stations);
  1241. //            $profile->avatar['path'] = $this->responsiveAssetsService->getResponsiveImageUrl($profile->avatar['path'], 'profile_media', $imageSize, 'jpg');
  1242. //            $profile->uri = $this->generateUrl('profile_preview.page', ['city' => $profile->city->uriIdentity, 'profile' => $profile->uriIdentity]);
  1243. //            return $profile;
  1244. //        }, $queryPage->getArray());
  1245. //
  1246. //        return new JsonResponse([
  1247. //            'profiles' => $profiles,
  1248. //            'currentPage' => $queryPage->getCurrentPage(),
  1249. //        ], Response::HTTP_OK);
  1250. //    }
  1251. }