src/Controller/ProfileListController.php line 218

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