src/Repository/ProfileRepository.php line 1171

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-03-19
  5.  * Time: 22:23
  6.  */
  7. namespace App\Repository;
  8. use App\Entity\Account\Advertiser;
  9. use App\Entity\Location\City;
  10. use App\Entity\Location\MapCoordinate;
  11. use App\Entity\Profile\Genders;
  12. use App\Entity\Profile\Photo;
  13. use App\Entity\Profile\Profile;
  14. use App\Entity\Sales\Profile\AdBoardPlacement;
  15. use App\Entity\Sales\Profile\AdBoardPlacementType;
  16. use App\Entity\Sales\Profile\PlacementHiding;
  17. use App\Entity\Sales\Profile\TopPlacement;
  18. use App\Entity\Saloon\Saloon;
  19. use App\Entity\User;
  20. use App\Repository\ReadModel\CityReadModel;
  21. use App\Repository\ReadModel\ProfileApartmentPricingReadModel;
  22. use App\Repository\ReadModel\ProfileListingReadModel;
  23. use App\Repository\ReadModel\ProfileMapReadModel;
  24. use App\Repository\ReadModel\ProfilePersonParametersReadModel;
  25. use App\Repository\ReadModel\ProfilePlacementHidingDetailReadModel;
  26. use App\Repository\ReadModel\ProfilePlacementPriceDetailReadModel;
  27. use App\Repository\ReadModel\ProfileTakeOutPricingReadModel;
  28. use App\Repository\ReadModel\ProvidedServiceReadModel;
  29. use App\Repository\ReadModel\StationLineReadModel;
  30. use App\Repository\ReadModel\StationReadModel;
  31. use App\Service\Features;
  32. use App\Service\Map\MapClusterMinPriceDql;
  33. use App\Specification\Profile\ProfileIdINOrderedByINValues;
  34. use App\Specification\Profile\ProfileIsActive;
  35. use App\Specification\Profile\ProfileIsNotHidden;
  36. use App\Specification\Profile\ProfileIsModerationPassed;
  37. use App\Specification\Profile\ProfileIsNotRejected;
  38. use Happyr\DoctrineSpecification\Spec;
  39. use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
  40. use Doctrine\DBAL\Connection;
  41. use Doctrine\ORM\AbstractQuery;
  42. use Doctrine\Persistence\ManagerRegistry;
  43. use Doctrine\DBAL\Statement;
  44. use Doctrine\ORM\QueryBuilder;
  45. use Happyr\DoctrineSpecification\Filter\Filter;
  46. use Happyr\DoctrineSpecification\Query\QueryModifier;
  47. use Porpaginas\Doctrine\ORM\ORMQueryResult;
  48. class ProfileRepository extends ServiceEntityRepository
  49. {
  50.     use SpecificationTrait;
  51.     use EntityIteratorTrait;
  52.     private Features $features;
  53.     private DistrictRepository $districts;
  54.     public function __construct(ManagerRegistry $registry, Features $features, DistrictRepository $districts)
  55.     {
  56.         parent::__construct($registry, Profile::class);
  57.         $this->features = $features;
  58.         $this->districts = $districts;
  59.     }
  60.     /**
  61.      * Возвращает итератор по данным, необходимым для генерации файлов sitemap, в виде массивов с
  62.      * следующими ключами:
  63.      *  - id
  64.      *  - uri
  65.      *  - updatedAt
  66.      *  - city_uri
  67.      *
  68.      * @return iterable<array{id: int, uri: string, updatedAt: \DateTimeImmutable, city_uri: string}>
  69.      */
  70.     public function sitemapItemsIterator(): iterable
  71.     {
  72.         $qb = $this->createQueryBuilder('profile')
  73.             ->select('profile.id, profile.uriIdentity AS uri, profile.updatedAt, city.uriIdentity AS city_uri')
  74.             ->join('profile.city', 'city')
  75.             ->andWhere('profile.deletedAt IS NULL');
  76.         $this->addModerationFilterToQb($qb, 'profile');
  77.         return $qb->getQuery()->toIterable([], AbstractQuery::HYDRATE_ARRAY);
  78.     }
  79.     protected function addModerationFilterToQb(QueryBuilder $qb, string $dqlAlias): void
  80.     {
  81.         if ($this->features->hard_moderation()) {
  82.             $qb->leftJoin(sprintf('%s.owner', $dqlAlias), 'owner');
  83.             $qb->andWhere(
  84.                 $qb->expr()->orX(
  85.                     sprintf('%s.moderationStatus = :status_passed', $dqlAlias),
  86.                     $qb->expr()->andX(
  87.                         sprintf('%s.moderationStatus = :status_waiting', $dqlAlias),
  88.                         'owner.trusted = true'
  89.                     )
  90.                 )
  91.             );
  92.             $qb->setParameter('status_passed', Profile::MODERATION_STATUS_APPROVED);
  93.             $qb->setParameter('status_waiting', Profile::MODERATION_STATUS_WAITING);
  94.         } else {
  95.             $qb->andWhere(sprintf('%s.moderationStatus IN (:statuses)', $dqlAlias));
  96.             $qb->setParameter('statuses', [Profile::MODERATION_STATUS_NOT_PASSED, Profile::MODERATION_STATUS_WAITING, Profile::MODERATION_STATUS_APPROVED]);
  97.         }
  98.     }
  99.     /**
  100.      * Число анкет для sitemap-фида. Условие совпадает с {@see findVisibleForSitemapShard()}
  101.      * и с {@see \App\Service\Sitemap\SitemapProfileVisibility}.
  102.      */
  103.     public function countVisibleForSitemap(): int
  104.     {
  105.         return (int) $this->createVisibleForSitemapQueryBuilder()
  106.             ->select('COUNT(profile.id)')
  107.             ->getQuery()
  108.             ->getSingleScalarResult();
  109.     }
  110.     /**
  111.      * Шард анкет для sitemap-фида. Порядок детерминирован: ORDER BY profile.id ASC.
  112.      *
  113.      * Анкета входит в фид, если одновременно:
  114.      * - deleteMode = 0 (любой deleteMode > 0 исключает, даже при deletedAt IS NULL);
  115.      * - deletedAt IS NULL (soft-delete исключает независимо от даты; grace-period не учитывается);
  116.      * - модерация как в {@see addModerationFilterToQb()} — эквивалент
  117.      *   ModerationService::isProfileEligibleToShowByModeration().
  118.      *   MODERATION_STATUS_REJECTED не входит, хотя ProfilePreviewController его показывает;
  119.      * - нет PlacementHiding (скрытые не входят, независимо от features->free_profiles());
  120.      * - активна: есть AdBoardPlacement или TopPlacement с placedAt <= now <= expiresAt
  121.      *   (как Profile::isActive()).
  122.      *
  123.      * @return list<array{id: int, uriIdentity: string, masseur: bool, updatedAt: \DateTimeInterface|null, city: string}>
  124.      */
  125.     public function findVisibleForSitemapShard(int $offset, int $limit): array
  126.     {
  127.         return $this->createVisibleForSitemapQueryBuilder()
  128.             ->select('profile.id AS id')
  129.             ->addSelect('profile.uriIdentity AS uriIdentity')
  130.             ->addSelect('profile.masseur AS masseur')
  131.             ->addSelect('profile.updatedAt AS updatedAt')
  132.             ->addSelect('c.uriIdentity AS city')
  133.             ->orderBy('profile.id', 'ASC')
  134.             ->setFirstResult($offset)
  135.             ->setMaxResults($limit)
  136.             ->getQuery()
  137.             ->getArrayResult();
  138.     }
  139.     /**
  140.      * Общий WHERE sitemap-фида анкет. Не отключает not_deleted_profile_filter:
  141.      * явное deletedAt IS NULL совпадает с этим фильтром.
  142.      */
  143.     private function createVisibleForSitemapQueryBuilder(): QueryBuilder
  144.     {
  145.         $qb = $this->createQueryBuilder('profile')
  146.             ->join('profile.city', 'c')
  147.             ->andWhere('profile.deleteMode = 0')
  148.             ->andWhere('profile.deletedAt IS NULL');
  149.         $this->addModerationFilterToQb($qb, 'profile');
  150.         $em = $qb->getEntityManager();
  151.         $hidden = new QueryBuilder($em);
  152.         $hidden->select('sitemap_profile_hiding');
  153.         $hidden->from(PlacementHiding::class, 'sitemap_profile_hiding');
  154.         $hidden->andWhere('sitemap_profile_hiding.profile = profile');
  155.         $adBoard = new QueryBuilder($em);
  156.         $adBoard->select('sitemap_profile_adboard');
  157.         $adBoard->from(AdBoardPlacement::class, 'sitemap_profile_adboard');
  158.         $adBoard->andWhere('sitemap_profile_adboard.profile = profile');
  159.         $top = new QueryBuilder($em);
  160.         $top->select('sitemap_profile_top');
  161.         $top->from(TopPlacement::class, 'sitemap_profile_top');
  162.         $top->andWhere('sitemap_profile_top.profile = profile');
  163.         $top->andWhere('sitemap_profile_top.placedAt <= :sitemap_now');
  164.         $top->andWhere('sitemap_profile_top.expiresAt >= :sitemap_now');
  165.         $qb->andWhere($qb->expr()->not($qb->expr()->exists($hidden->getDQL())));
  166.         $qb->andWhere($qb->expr()->orX(
  167.             $qb->expr()->exists($adBoard->getDQL()),
  168.             $qb->expr()->exists($top->getDQL()),
  169.         ));
  170.         $qb->setParameter('sitemap_now', new \DateTimeImmutable());
  171.         return $qb;
  172.     }
  173.     public function ofUriIdentityWithinCity(string $uriIdentity, City $city): ?Profile
  174.     {
  175.         return $this->findOneBy([
  176.             'uriIdentity' => $uriIdentity,
  177.             'city' => $city,
  178.         ]);
  179.     }
  180.     /**
  181.      * Метод проверки уникальности анкет по URI не должен использовать никаких фильтров, кроме URI и города,
  182.      * поэтому QueryBuilder не используется
  183.      * @see https://redminez.net/issues/27310
  184.      */
  185.     public function isUniqueUriIdentityExistWithinCity(string $uriIdentity, City $city): bool
  186.     {
  187.         $connection = $this->_em->getConnection();
  188.         $stmt = $connection->executeQuery('SELECT COUNT(id) FROM profiles WHERE uri_identity = ? AND city_id = ?', [$uriIdentity, $city->getId()]);
  189.         $count = $stmt->fetchOne();
  190.         return $count > 0;
  191.     }
  192.     public function countByCity(): array
  193.     {
  194.         $qb = $this->createQueryBuilder('profile')
  195.             ->select('IDENTITY(profile.city), COUNT(profile.id)')
  196.             ->groupBy('profile.city');
  197.         $this->addFemaleGenderFilterToQb($qb, 'profile');
  198.         $this->addModerationFilterToQb($qb, 'profile');
  199.         //$this->excludeHavingPlacementHiding($qb, 'profile');
  200.         $this->havingAdBoardPlacement($qb, 'profile');
  201.         $query = $qb->getQuery()
  202.             ->useResultCache(true)
  203.             ->setResultCacheLifetime(120);
  204.         $rawResult = $query->getScalarResult();
  205.         $indexedResult = [];
  206.         foreach ($rawResult as $row) {
  207.             $indexedResult[$row[1]] = $row[2];
  208.         }
  209.         return $indexedResult;
  210.     }
  211.     protected function addFemaleGenderFilterToQb(QueryBuilder $qb, string $alias): void
  212.     {
  213.         $this->addGenderFilterToQb($qb, $alias, [Genders::FEMALE]);
  214.     }
  215.     protected function addGenderFilterToQb(QueryBuilder $qb, string $alias, array $genders = [Genders::FEMALE]): void
  216.     {
  217.         $qb->andWhere(sprintf('%s.personParameters.gender IN (:genders)', $alias));
  218.         $qb->setParameter('genders', $genders);
  219.     }
  220.     private function havingAdBoardPlacement(QueryBuilder $qb, string $alias): void
  221.     {
  222.         $qb->join(sprintf('%s.adBoardPlacement', $alias), 'adboard_placement');
  223.     }
  224.     public function countByStations(): array
  225.     {
  226.         $qb = $this->createQueryBuilder('profiles')
  227.             ->select('stations.id, COUNT(profiles.id) as cnt')
  228.             ->join('profiles.stations', 'stations')
  229.             //это условие сильно затормжаживает запрос, но оно и не нужно при условии, что чужих(от других городов) станций у анкеты нет
  230.             //->where('profiles.city = stations.city')
  231.             ->groupBy('stations.id');
  232.         $this->addFemaleGenderFilterToQb($qb, 'profiles');
  233.         $this->addModerationFilterToQb($qb, 'profiles');
  234.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  235.         $this->havingAdBoardPlacement($qb, 'profiles');
  236.         $query = $qb->getQuery()
  237.             ->useResultCache(true)
  238.             ->setResultCacheLifetime(120);
  239.         $rawResult = $query->getScalarResult();
  240.         $indexedResult = [];
  241.         foreach ($rawResult as $row) {
  242.             $indexedResult[$row['id']] = $row['cnt'];
  243.         }
  244.         return $indexedResult;
  245.     }
  246.     public function countByDistricts(): array
  247.     {
  248.         $qb = $this->createQueryBuilder('profiles')
  249.             ->select('districts.id, COUNT(profiles.id) as cnt')
  250.             ->join('profiles.stations', 'stations')
  251.             ->join('stations.district', 'districts')
  252.             ->groupBy('districts.id');
  253.         $this->addFemaleGenderFilterToQb($qb, 'profiles');
  254.         $this->addModerationFilterToQb($qb, 'profiles');
  255.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  256.         $this->havingAdBoardPlacement($qb, 'profiles');
  257.         $query = $qb->getQuery()
  258.             ->useResultCache(true)
  259.             ->setResultCacheLifetime(120);
  260.         $rawResult = $query->getScalarResult();
  261.         $indexedResult = [];
  262.         foreach ($rawResult as $row) {
  263.             $indexedResult[$row['id']] = $row['cnt'];
  264.         }
  265.         return $indexedResult;
  266.     }
  267.     public function countByCounties(): array
  268.     {
  269.         $qb = $this->createQueryBuilder('profiles')
  270.             ->select('counties.id, COUNT(profiles.id) as cnt')
  271.             ->join('profiles.stations', 'stations')
  272.             ->join('stations.district', 'districts')
  273.             ->join('districts.county', 'counties')
  274.             ->groupBy('counties.id');
  275.         $this->addFemaleGenderFilterToQb($qb, 'profiles');
  276.         $this->addModerationFilterToQb($qb, 'profiles');
  277.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  278.         $this->havingAdBoardPlacement($qb, 'profiles');
  279.         $query = $qb->getQuery()
  280.             ->useResultCache(true)
  281.             ->setResultCacheLifetime(120);
  282.         $rawResult = $query->getScalarResult();
  283.         $indexedResult = [];
  284.         foreach ($rawResult as $row) {
  285.             $indexedResult[$row['id']] = $row['cnt'];
  286.         }
  287.         return $indexedResult;
  288.     }
  289.     /**
  290.      * @param array|int[] $ids
  291.      * @return Profile[]
  292.      */
  293.     public function findByIds(array $ids): array
  294.     {
  295.         return $this->createQueryBuilder('profile')
  296.             ->andWhere('profile.id IN (:ids)')
  297.             ->setParameter('ids', $ids)
  298.             ->orderBy('FIELD(profile.id,:ids2)')
  299.             ->setParameter('ids2', $ids)
  300.             ->getQuery()
  301.             ->getResult();
  302.     }
  303.     public function findByIdsIterate(array $ids): iterable
  304.     {
  305.         $qb = $this->createQueryBuilder('profile')
  306.             ->andWhere('profile.id IN (:ids)')
  307.             ->setParameter('ids', $ids)
  308.             ->orderBy('FIELD(profile.id,:ids2)')
  309.             ->setParameter('ids2', $ids);
  310.         return $this->iterateQueryBuilder($qb);
  311.     }
  312.     /**
  313.      * Список анкет указанного типа (массажистки или нет), привязанных к аккаунту
  314.      */
  315.     public function ofOwnerAndTypePaged(User $owner, bool $masseurs): ORMQueryResult
  316.     {
  317.         $qb = $this->createQueryBuilder('profile')
  318.             ->andWhere('profile.owner = :owner')
  319.             ->setParameter('owner', $owner)
  320.             ->andWhere('profile.masseur = :is_masseur')
  321.             ->setParameter('is_masseur', $masseurs);
  322.         return new ORMQueryResult($qb);
  323.     }
  324.     /**
  325.      * Список активных анкет, привязанных к аккаунту
  326.      */
  327.     public function activeAndOwnedBy(User $owner): ORMQueryResult
  328.     {
  329.         $qb = $this->createQueryBuilder('profile')
  330.             ->join('profile.adBoardPlacement', 'profile_adboard_placement')
  331.             ->andWhere('profile.owner = :owner')
  332.             ->setParameter('owner', $owner);
  333.         return new ORMQueryResult($qb);
  334.     }
  335.     /**
  336.      * Список активных или скрытых анкет, привязанных к аккаунту
  337.      *
  338.      * @return Profile[]|ORMQueryResult
  339.      */
  340.     public function activeOrHiddenAndOwnedBy(User $owner): ORMQueryResult
  341.     {
  342.         $qb = $this->createQueryBuilder('profile')
  343.             ->leftJoin('profile.adBoardPlacement', 'profile_adboard_placement')
  344.             ->leftJoin('profile.placementHiding', 'placement_hiding')
  345.             ->andWhere('profile_adboard_placement IS NOT NULL OR placement_hiding IS NOT NULL')
  346.             ->andWhere('profile.owner = :owner')
  347.             ->setParameter('owner', $owner);
  348.         return new ORMQueryResult($qb);
  349.     }
  350.     public function activePaidAdBoardPlacementAndOwnedBy(User $owner): ORMQueryResult
  351.     {
  352.         $qb = $this->createQueryBuilder('profile')
  353.             ->addSelect('profile_adboard_placement', 'placement_price', 'city', 'owner')
  354.             ->join('profile.adBoardPlacement', 'profile_adboard_placement')
  355.             ->leftJoin('profile_adboard_placement.placementPrice', 'placement_price')
  356.             ->join('profile.city', 'city')
  357.             ->join('profile.owner', 'owner')
  358.             ->andWhere('profile_adboard_placement.type <> :free_placement_type')
  359.             ->andWhere('profile.owner = :owner')
  360.             ->setParameter('free_placement_type', AdBoardPlacementType::FREE)
  361.             ->setParameter('owner', $owner);
  362.         return new ORMQueryResult($qb);
  363.     }
  364.     public function paidAdBoardPlacementChargeRowsOfOwner(User $owner): array
  365.     {
  366.         $qb = $this->createQueryBuilder('profile')
  367.             ->select([
  368.                 'profile.id AS profile_id',
  369.                 'profile.approved AS approved',
  370.                 'profile.masseur AS is_masseur',
  371.                 'profile.personParameters.gender AS gender',
  372.                 'profile_adboard_placement.type AS placement_type',
  373.                 'profile_adboard_placement.planManaged AS plan_managed',
  374.                 'placement_price.id AS placement_price_id',
  375.                 'placement_price.priceAmount AS price_amount',
  376.                 'placement_price.duration AS duration',
  377.                 'placement_price.currency AS currency',
  378.                 'placement_price.dynamicPriceMatrix AS dynamic_price_matrix',
  379.                 'city.id AS city_id',
  380.                 'city.cityPriceCategory AS city_price_category',
  381.                 'city.timezone AS timezone',
  382.                 'owner.currencyCode AS owner_currency',
  383.             ])
  384.             ->join('profile.adBoardPlacement', 'profile_adboard_placement')
  385.             ->leftJoin('profile_adboard_placement.placementPrice', 'placement_price')
  386.             ->join('profile.city', 'city')
  387.             ->join('profile.owner', 'owner')
  388.             ->andWhere('profile_adboard_placement.type <> :free_placement_type')
  389.             ->andWhere('profile.owner = :owner')
  390.             ->setParameter('free_placement_type', AdBoardPlacementType::FREE)
  391.             ->setParameter('owner', $owner);
  392.         return $qb->getQuery()->getArrayResult();
  393.     }
  394.     public function currentChargeableAndOwnedBy(User $owner): ORMQueryResult
  395.     {
  396.         $qb = $this->createQueryBuilder('profile')
  397.             ->addSelect('profile_adboard_placement', 'placement_price', 'placement_hiding', 'city', 'owner')
  398.             ->leftJoin('profile.adBoardPlacement', 'profile_adboard_placement')
  399.             ->leftJoin('profile_adboard_placement.placementPrice', 'placement_price')
  400.             ->leftJoin('profile.placementHiding', 'placement_hiding')
  401.             ->join('profile.city', 'city')
  402.             ->join('profile.owner', 'owner')
  403.             ->andWhere('(profile_adboard_placement IS NOT NULL AND profile_adboard_placement.type <> :free_placement_type) OR placement_hiding IS NOT NULL')
  404.             ->andWhere('profile.owner = :owner')
  405.             ->setParameter('free_placement_type', AdBoardPlacementType::FREE)
  406.             ->setParameter('owner', $owner);
  407.         return new ORMQueryResult($qb);
  408.     }
  409.     public function iterateStale30dMarketingCandidates(\DateTimeImmutable $cutoff): \Generator
  410.     {
  411.         $qb = $this->createQueryBuilder('profile')
  412.             ->addSelect('owner', 'city', 'ownerCity')
  413.             ->join('profile.owner', 'owner')
  414.             ->join('profile.city', 'city')
  415.             ->leftJoin('owner.city', 'ownerCity')
  416.             ->andWhere('owner.enabled = true')
  417.             ->andWhere('profile.mauticStale30dSentAt IS NULL')
  418.             ->andWhere('COALESCE(profile.updatedAt, profile.createdAt) < :cutoff')
  419.             ->setParameter('cutoff', $cutoff)
  420.             ->orderBy('owner.id', 'ASC')
  421.             ->addOrderBy('profile.id', 'ASC');
  422.         foreach ($qb->getQuery()->toIterable() as $profile) {
  423.             yield $profile;
  424.         }
  425.     }
  426.     public function countFreeUnapprovedLimited(): int
  427.     {
  428.         $qb = $this->createQueryBuilder('profile')
  429.             ->select('count(profile)')
  430.             ->join('profile.adBoardPlacement', 'placement')
  431.             ->andWhere('placement.type = :placement_type')
  432.             ->setParameter('placement_type', AdBoardPlacementType::FREE)
  433.             ->leftJoin('profile.placementHiding', 'hiding')
  434.             ->andWhere('hiding IS NULL')
  435.             ->andWhere('profile.approved = false');
  436.         return (int)$qb->getQuery()->getSingleScalarResult();
  437.     }
  438.     public function iterateFreeUnapprovedLimited(int $limit): iterable
  439.     {
  440.         $qb = $this->createQueryBuilder('profile')
  441.             ->join('profile.adBoardPlacement', 'placement')
  442.             ->andWhere('placement.type = :placement_type')
  443.             ->setParameter('placement_type', AdBoardPlacementType::FREE)
  444.             ->leftJoin('profile.placementHiding', 'hiding')
  445.             ->andWhere('hiding IS NULL')
  446.             ->andWhere('profile.approved = false')
  447.             ->setMaxResults($limit);
  448.         return $this->iterateQueryBuilder($qb);
  449.     }
  450.     /**
  451.      * Число активных анкет, привязанных к аккаунту
  452.      */
  453.     public function countActiveOfOwner(User $owner, ?bool $isMasseur = false): int
  454.     {
  455.         $qb = $this->createQueryBuilder('profile')
  456.             ->select('COUNT(profile.id)')
  457.             ->join('profile.adBoardPlacement', 'profile_adboard_placement')
  458.             ->andWhere('profile.owner = :owner')
  459.             ->setParameter('owner', $owner);
  460.         if ($this->features->hard_moderation()) {
  461.             $qb->leftJoin('profile.owner', 'owner');
  462.             $qb->andWhere(
  463.                 $qb->expr()->orX(
  464.                     'profile.moderationStatus = :status_passed',
  465.                     $qb->expr()->andX(
  466.                         'profile.moderationStatus = :status_waiting',
  467.                         'owner.trusted = true'
  468.                     )
  469.                 )
  470.             );
  471.             $qb->setParameter('status_passed', Profile::MODERATION_STATUS_APPROVED);
  472.             $qb->setParameter('status_waiting', Profile::MODERATION_STATUS_WAITING);
  473.         } else {
  474.             $qb->andWhere('profile.moderationStatus IN (:statuses)')
  475.                 ->setParameter('statuses', [Profile::MODERATION_STATUS_NOT_PASSED, Profile::MODERATION_STATUS_WAITING, Profile::MODERATION_STATUS_APPROVED]);
  476.         }
  477.         if (null !== $isMasseur) {
  478.             $qb->andWhere('profile.masseur = :is_masseur')
  479.                 ->setParameter('is_masseur', $isMasseur);
  480.         }
  481.         return (int)$qb->getQuery()->getSingleScalarResult();
  482.     }
  483.     /**
  484.      * Число всех анкет, привязанных к аккаунту
  485.      */
  486.     public function countAllOfOwnerNotDeleted(User $owner, ?bool $isMasseur = false): int
  487.     {
  488.         $qb = $this->createQueryBuilder('profile')
  489.             ->select('COUNT(profile.id)')
  490.             ->andWhere('profile.owner = :owner')
  491.             ->setParameter('owner', $owner)
  492.             //потому что используется в т.ч. на тех страницах, где отключен фильтр вывода "только неудаленных"
  493.             ->andWhere('profile.deletedAt IS NULL');
  494.         if (null !== $isMasseur) {
  495.             $qb->andWhere('profile.masseur = :is_masseur')
  496.                 ->setParameter('is_masseur', $isMasseur);
  497.         }
  498.         return (int)$qb->getQuery()->getSingleScalarResult();
  499.     }
  500.     public function findPreviewByOwner(Advertiser $owner, int $limit): array
  501.     {
  502.         return $this->createQueryBuilder('profile')
  503.             ->addSelect('city')
  504.             ->join('profile.city', 'city')
  505.             ->andWhere('profile.owner = :owner')
  506.             ->andWhere('profile.deletedAt IS NULL')
  507.             ->setParameter('owner', $owner)
  508.             ->orderBy('profile.id', 'DESC')
  509.             ->setMaxResults($limit)
  510.             ->getQuery()
  511.             ->getResult();
  512.     }
  513.     public function getTimezonesListByUser(User $owner): array
  514.     {
  515.         $q = $this->_em->createQuery(sprintf("
  516.                 SELECT c
  517.                 FROM %s c
  518.                 WHERE c.id IN (
  519.                     SELECT DISTINCT(c2.id) 
  520.                     FROM %s p
  521.                     JOIN p.city c2
  522.                     WHERE p.owner = :user
  523.                 )
  524.             ", $this->_em->getClassMetadata(City::class)->name, $this->_em->getClassMetadata(Profile::class)->name))
  525.             ->setParameter('user', $owner);
  526.         return $q->getResult();
  527.     }
  528.     /**
  529.      * Список анкет, привязанных к аккаунту
  530.      *
  531.      * @return Profile[]
  532.      */
  533.     public function ofOwner(User $owner): array
  534.     {
  535.         $qb = $this->createQueryBuilder('profile')
  536.             ->andWhere('profile.owner = :owner')
  537.             ->setParameter('owner', $owner);
  538.         return $qb->getQuery()->getResult();
  539.     }
  540.     public function ofOwnerPaged(User $owner, array $genders = [Genders::FEMALE]): ORMQueryResult
  541.     {
  542.         $qb = $this->createQueryBuilder('profile')
  543.             ->andWhere('profile.owner = :owner')
  544.             ->setParameter('owner', $owner)
  545.             ->andWhere('profile.personParameters.gender IN (:genders)')
  546.             ->setParameter('genders', $genders);
  547.         return new ORMQueryResult($qb);
  548.     }
  549.     public function searchLinkableToSaloonByOwner(User $owner, ?string $query, int $limit = 20): array
  550.     {
  551.         $qb = $this->createQueryBuilder('profile')
  552.             ->andWhere('profile.owner = :owner')
  553.             ->setParameter('owner', $owner)
  554.             ->orderBy('profile.id', 'DESC')
  555.             ->setMaxResults($limit)
  556.         ;
  557.         if ($query) {
  558.             $qb
  559.                 ->andWhere('LOWER(JSON_UNQUOTE(JSON_EXTRACT(profile.name, :json_path))) LIKE :query')
  560.                 ->setParameter('json_path', '$.ru')
  561.                 ->setParameter('query', '%' . addcslashes(mb_strtolower(trim($query)), '%_') . '%')
  562.             ;
  563.         }
  564.         return $qb->getQuery()->getResult();
  565.     }
  566.     public function findLinkableToSaloonByOwnerAndIds(User $owner, array $ids): array
  567.     {
  568.         $ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
  569.         if (empty($ids)) {
  570.             return [];
  571.         }
  572.         return $this->createQueryBuilder('profile')
  573.             ->andWhere('profile.owner = :owner')
  574.             ->andWhere('profile.id IN (:ids)')
  575.             ->setParameter('owner', $owner)
  576.             ->setParameter('ids', $ids)
  577.             ->getQuery()
  578.             ->getResult()
  579.         ;
  580.     }
  581.     public function findPublicProfilesBySaloon(Saloon $saloon, int $limit = 6, int $offset = 0): array
  582.     {
  583.         $profiles = $this->createPublicProfilesBySaloonQueryBuilder($saloon)
  584.             ->addSelect('placement')
  585.             ->orderBy('profile.id', 'DESC')
  586.             ->setMaxResults($limit)
  587.             ->setFirstResult($offset)
  588.             ->getQuery()
  589.             ->getResult()
  590.         ;
  591.         $this->loadPublicProfilePreviewRelations($profiles);
  592.         return $profiles;
  593.     }
  594.     public function countPublicProfilesBySaloon(Saloon $saloon): int
  595.     {
  596.         return (int)$this->createPublicProfilesBySaloonQueryBuilder($saloon)
  597.             ->select('COUNT(DISTINCT profile.id)')
  598.             ->getQuery()
  599.             ->getSingleScalarResult()
  600.         ;
  601.     }
  602.     public function findPublicProfilesBySaloonCircular(Saloon $saloon, int $limit, int $offset, int $total): array
  603.     {
  604.         if ($total <= 0 || $limit <= 0) {
  605.             return [];
  606.         }
  607.         $offset %= $total;
  608.         $firstChunkLimit = min($limit, $total - $offset);
  609.         $profiles = $this->findPublicProfilesBySaloon($saloon, $firstChunkLimit, $offset);
  610.         if (count($profiles) < $limit && $offset > 0) {
  611.             $profiles = array_merge(
  612.                 $profiles,
  613.                 $this->findPublicProfilesBySaloon($saloon, $limit - count($profiles), 0)
  614.             );
  615.         }
  616.         return $profiles;
  617.     }
  618.     public function findPublicProfilesBySaloonRotatedByPlacementStatus(Saloon $saloon, int $limit, int $offset, int $rotationSeed): array
  619.     {
  620.         if ($limit <= 0) {
  621.             return [];
  622.         }
  623.         $profiles = $this->createPublicProfilesBySaloonQueryBuilder($saloon)
  624.             ->addSelect('placement')
  625.             ->orderBy('placement.type', 'DESC')
  626.             ->addOrderBy('placement.placedAt', 'DESC')
  627.             ->addOrderBy('profile.id', 'DESC')
  628.             ->getQuery()
  629.             ->getResult()
  630.         ;
  631.         $profiles = array_slice($this->rotateProfilesWithinPlacementTypes($profiles, $rotationSeed), $offset, $limit);
  632.         $this->loadPublicProfilePreviewRelations($profiles);
  633.         return $profiles;
  634.     }
  635.     private function rotateProfilesWithinPlacementTypes(array $profiles, int $rotationSeed): array
  636.     {
  637.         $profilesByPlacementType = [];
  638.         foreach ($profiles as $profile) {
  639.             $profilesByPlacementType[$this->getProfilePlacementPriority($profile)][] = $profile;
  640.         }
  641.         krsort($profilesByPlacementType, SORT_NUMERIC);
  642.         $rotatedProfiles = [];
  643.         foreach ($profilesByPlacementType as $profilesGroup) {
  644.             $profilesGroupCount = count($profilesGroup);
  645.             $groupOffset = $profilesGroupCount > 0 ? $rotationSeed % $profilesGroupCount : 0;
  646.             if (0 === $groupOffset) {
  647.                 $rotatedProfiles = array_merge($rotatedProfiles, $profilesGroup);
  648.                 continue;
  649.             }
  650.             $rotatedProfiles = array_merge(
  651.                 $rotatedProfiles,
  652.                 array_slice($profilesGroup, $groupOffset),
  653.                 array_slice($profilesGroup, 0, $groupOffset)
  654.             );
  655.         }
  656.         return $rotatedProfiles;
  657.     }
  658.     private function getProfilePlacementPriority(Profile $profile): int
  659.     {
  660.         $placement = $profile->getAdBoardPlacement();
  661.         return $placement instanceof AdBoardPlacement ? $placement->getType()->getValue() : 0;
  662.     }
  663.     private function createPublicProfilesBySaloonQueryBuilder(Saloon $saloon): QueryBuilder
  664.     {
  665.         return $this->createQueryBuilder('profile')
  666.             ->leftJoin('profile.adBoardPlacement', 'placement')
  667.             ->leftJoin('profile.placementHiding', 'placement_hiding')
  668.             ->andWhere('profile.saloon = :saloon')
  669.             ->andWhere('profile.moderationStatus = :moderation_status')
  670.             ->andWhere('placement_hiding IS NULL')
  671.             ->setParameter('saloon', $saloon)
  672.             ->setParameter('moderation_status', Profile::MODERATION_STATUS_APPROVED)
  673.         ;
  674.     }
  675.     private function loadPublicProfilePreviewRelations(array $profiles): void
  676.     {
  677.         if (empty($profiles)) {
  678.             return;
  679.         }
  680.         $this->createQueryBuilder('profile')
  681.             ->leftJoin('profile.city', 'city')
  682.             ->leftJoin('profile.stations', 'station')
  683.             ->leftJoin('profile.avatar', 'avatar')
  684.             ->leftJoin('profile.photos', 'photo')
  685.             ->addSelect('city')
  686.             ->addSelect('station')
  687.             ->addSelect('avatar')
  688.             ->addSelect('photo')
  689.             ->andWhere('profile IN (:profiles)')
  690.             ->setParameter('profiles', $profiles)
  691.             ->getQuery()
  692.             ->getResult()
  693.         ;
  694.     }
  695.     public function ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterIterateAll(User $owner, string $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur = null): \Generator
  696.     {
  697.         $query = $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner, $placementTypeFilter, $nameFilter, $isMasseur)->getQuery();
  698.         foreach ($query->iterate() as $row) {
  699.             yield $row[0];
  700.         }
  701.     }
  702.     private function queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $owner, string $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur = null): QueryBuilder
  703.     {
  704.         $qb = $this->createQueryBuilder('profile')
  705.             ->andWhere('profile.owner = :owner')
  706.             ->setParameter('owner', $owner);
  707.         switch ($placementTypeFilter) {
  708.             case 'paid':
  709.                 $qb->join('profile.adBoardPlacement', 'placement')
  710.                     ->andWhere('placement.type != :placement_type')
  711.                     ->setParameter('placement_type', AdBoardPlacementType::FREE);
  712.                 break;
  713.             case 'free':
  714.                 $qb->join('profile.adBoardPlacement', 'placement')
  715.                     ->andWhere('placement.type = :placement_type')
  716.                     ->setParameter('placement_type', AdBoardPlacementType::FREE);
  717.                 break;
  718.             case 'ultra-vip':
  719.                 $qb->join('profile.adBoardPlacement', 'placement')
  720.                     ->andWhere('placement.type = :placement_type')
  721.                     ->setParameter('placement_type', AdBoardPlacementType::ULTRA_VIP);
  722.                 break;
  723.             case 'vip':
  724.                 $qb->join('profile.adBoardPlacement', 'placement')
  725.                     ->andWhere('placement.type = :placement_type')
  726.                     ->setParameter('placement_type', AdBoardPlacementType::VIP);
  727.                 break;
  728.             case 'standard':
  729.                 $qb->join('profile.adBoardPlacement', 'placement')
  730.                     ->andWhere('placement.type = :placement_type')
  731.                     ->setParameter('placement_type', AdBoardPlacementType::STANDARD);
  732.                 break;
  733.             case 'hidden':
  734.                 $qb->join('profile.placementHiding', 'placement_hiding');
  735.                 break;
  736.             case 'all':
  737.             default:
  738.                 break;
  739.         }
  740.         if ($nameFilter) {
  741.             $nameExpr = $qb->expr()->orX(
  742.                 'LOWER(JSON_UNQUOTE(JSON_EXTRACT(profile.name, :jsonPath))) LIKE :name_filter',
  743.                 \sprintf("REGEXP_REPLACE(profile.phoneNumber, '-| ', '') LIKE :name_filter"),
  744.                 'LOWER(profile.phoneNumber) LIKE :name_filter',
  745.                 \sprintf("REGEXP_REPLACE(profile.phoneNumber, '\+7', '8') LIKE :name_filter"),
  746.             );
  747.             $qb->setParameter('jsonPath', '$.ru');
  748.             $qb->setParameter('name_filter', '%' . addcslashes(mb_strtolower(str_replace(['(', ')', ' ', '-'], '', $nameFilter)), '%_') . '%');
  749.             $qb->andWhere($nameExpr);
  750.         }
  751.         if (null !== $isMasseur) {
  752.             $qb->andWhere('profile.masseur = :is_masseur')
  753.                 ->setParameter('is_masseur', $isMasseur);
  754.         }
  755.         return $qb;
  756.     }
  757.     public function ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterPaged(User $owner, string $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur = null): ORMQueryResult
  758.     {
  759.         $qb = $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner, $placementTypeFilter, $nameFilter, $isMasseur);
  760.         //сортируем анкеты по статусу UltraVip->Vip->Standard->Free->Hidden
  761.         $aliases = $qb->getAllAliases();
  762.         if (false == in_array('placement', $aliases))
  763.             $qb->leftJoin('profile.adBoardPlacement', 'placement');
  764.         if (false == in_array('placement_hiding', $aliases))
  765.             $qb->leftJoin('profile.placementHiding', 'placement_hiding');
  766.         $qb->addSelect('IF(placement_hiding.id IS NULL, 0, 1) as HIDDEN is_hidden');
  767.         $qb->addOrderBy('placement.type', 'DESC');
  768.         $qb->addOrderBy('placement.placedAt', 'DESC');
  769.         $qb->addOrderBy('is_hidden', 'ASC');
  770.         return new ORMQueryResult($qb);
  771.     }
  772.     public function idsOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $owner, string $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur = null): array
  773.     {
  774.         $qb = $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner, $placementTypeFilter, $nameFilter, $isMasseur);
  775.         $qb->select('profile.id');
  776.         return $qb->getQuery()->getResult('column_hydrator');
  777.     }
  778.     public function countOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $owner, string $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur = null): int
  779.     {
  780.         $qb = $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner, $placementTypeFilter, $nameFilter, $isMasseur);
  781.         $qb->select('count(profile.id)')
  782.             ->setMaxResults(1);
  783.         return (int)$qb->getQuery()->getSingleScalarResult();
  784.     }
  785.     /**
  786.      * @deprecated
  787.      */
  788.     public function hydrateProfileRow(array $row): ProfileListingReadModel
  789.     {
  790.         $profile = new ProfileListingReadModel();
  791.         $profile->id = $row['id'];
  792.         $profile->city = $row['city'];
  793.         $profile->uriIdentity = $row['uriIdentity'];
  794.         $profile->name = $row['name'];
  795.         $profile->description = $row['description'];
  796.         $profile->phoneNumber = $row['phoneNumber'];
  797.         $profile->isMasseur = $row['masseur'];
  798.         $profile->approved = $row['approved'];
  799.         $now = new \DateTimeImmutable('now');
  800.         $hasRunningTopPlacement = false;
  801.         foreach ($row['topPlacements'] as $topPlacement) {
  802.             if ($topPlacement['placedAt'] <= $now && $now <= $topPlacement['expiresAt'])
  803.                 $hasRunningTopPlacement = true;
  804.         }
  805.         $profile->active = null !== $row['adBoardPlacement'] || $hasRunningTopPlacement;
  806.         $profile->hidden = null != $row['placementHiding'];
  807.         $profile->personParameters = new ProfilePersonParametersReadModel();
  808.         $profile->personParameters->age = $row['personParameters.age'];
  809.         $profile->personParameters->height = $row['personParameters.height'];
  810.         $profile->personParameters->weight = $row['personParameters.weight'];
  811.         $profile->personParameters->breastSize = $row['personParameters.breastSize'];
  812.         $profile->personParameters->bodyType = $row['personParameters.bodyType'];
  813.         $profile->personParameters->hairColor = $row['personParameters.hairColor'];
  814.         $profile->personParameters->privateHaircut = $row['personParameters.privateHaircut'];
  815.         $profile->personParameters->nationality = $row['personParameters.nationality'];
  816.         $profile->personParameters->appearance = $row['personParameters.appearance'];
  817.         $profile->personParameters->hasTattoo = $row['personParameters.hasTattoo'];
  818.         $profile->personParameters->hasPiercing = $row['personParameters.hasPiercing'];
  819.         $profile->stations = $row['stations'];
  820.         $profile->avatar = $row['avatar'];
  821.         foreach ($row['photos'] as $photo)
  822.             if ($photo['main'])
  823.                 $profile->mainPhoto = $photo;
  824.         $profile->mainPhoto = null;
  825.         $profile->photos = [];
  826.         $profile->selfies = [];
  827.         foreach ($row['photos'] as $photo) {
  828.             if ($photo['main'])
  829.                 $profile->mainPhoto = $photo;
  830.             if ($photo['type'] == Photo::TYPE_PHOTO)
  831.                 $profile->photos[] = $photo;
  832.             if ($photo['type'] == Photo::TYPE_SELFIE)
  833.                 $profile->selfies[] = $photo;
  834.         }
  835.         $profile->videos = $row['videos'];
  836.         $profile->comments = $row['comments'];
  837.         $profile->apartmentsPricing = new ProfileApartmentPricingReadModel();
  838.         $profile->apartmentsPricing->oneHourPrice = $row['apartmentsPricing.oneHourPrice'];
  839.         $profile->apartmentsPricing->twoHoursPrice = $row['apartmentsPricing.twoHoursPrice'];
  840.         $profile->apartmentsPricing->nightPrice = $row['apartmentsPricing.nightPrice'];
  841.         $profile->takeOutPricing = new ProfileTakeOutPricingReadModel();
  842.         $profile->takeOutPricing->oneHourPrice = $row['takeOutPricing.oneHourPrice'];
  843.         $profile->takeOutPricing->twoHoursPrice = $row['takeOutPricing.twoHoursPrice'];
  844.         $profile->takeOutPricing->nightPrice = $row['takeOutPricing.nightPrice'];
  845.         return $profile;
  846.     }
  847.     public function deletedByPeriod(\DateTimeInterface $start, \DateTimeInterface $end): array
  848.     {
  849.         $qb = $this->createQueryBuilder('profile')
  850.             ->join('profile.city', 'city')
  851.             ->select('profile.uriIdentity _profile')
  852.             ->addSelect('city.uriIdentity _city')
  853.             ->andWhere('profile.deletedAt >= :start')
  854.             ->andWhere('profile.deletedAt <= :end')
  855.             ->setParameter('start', $start)
  856.             ->setParameter('end', $end);
  857.         return $qb->getQuery()->getResult();
  858.     }
  859.     public function listMapPointsMatchingSpec(Filter|QueryModifier $specification): array
  860.     {
  861.         $qb = $this->createQueryBuilder('p');
  862.         $qb->select('p.id AS id, p.mapCoordinate.latitude AS lat, p.mapCoordinate.longitude AS lng, p.masseur AS masseur')
  863.             ->addSelect(MapClusterMinPriceDql::entityMinPriceExpression('p').' AS minPrice');
  864.         $specification->modify($qb, 'p');
  865.         $qb->andWhere($specification->getFilter($qb, 'p'));
  866.         return $qb->getQuery()->getArrayResult();
  867.     }
  868.     public function listForMapMatchingSpec(Filter|QueryModifier $specification, int $coordinatesRoundPrecision = 3): array
  869.     {
  870.         $this->getEntityManager()->getConnection()->executeQuery("
  871.             SET SESSION group_concat_max_len = 100000;
  872.         ");
  873.         /** @var QueryBuilder $qb */
  874.         $qb = $this->createQueryBuilder($dqlAlias = 'p');
  875.         $qb->select(sprintf('GROUP_CONCAT(p.id), CONCAT(ROUND(MIN(p.mapCoordinate.latitude),5),\',\',ROUND(MIN(p.mapCoordinate.longitude),5)), count(p.id), CONCAT(ROUND(p.mapCoordinate.latitude,%1$s),\',\',ROUND(p.mapCoordinate.longitude,%1$s)) as coords, GROUP_CONCAT(p.masseur)', $coordinatesRoundPrecision));
  876.         $qb->groupBy('coords');
  877.         $specification->modify($qb, $dqlAlias);
  878.         $qb->andWhere($specification->getFilter($qb, $dqlAlias));
  879.         return $qb->getQuery()->getResult();
  880.     }
  881.     /**
  882.      * Clustered map points for JSON API mode=map.
  883.      * Representative point is the centroid (AVG), not MIN as in listForMapMatchingSpec().
  884.      */
  885.     public function listMapClustersMatchingSpec(Filter|QueryModifier $specification, int $coordinatesRoundPrecision): array
  886.     {
  887.         $this->getEntityManager()->getConnection()->executeQuery("
  888.             SET SESSION group_concat_max_len = 100000;
  889.         ");
  890.         $precision = (int) $coordinatesRoundPrecision;
  891.         /** @var QueryBuilder $qb */
  892.         $qb = $this->createQueryBuilder($dqlAlias = 'p');
  893.         $qb->select(sprintf(
  894.             '%s, '
  895.             . 'GROUP_CONCAT(p.id ORDER BY p.id) AS ids, '
  896.             . 'COUNT(p.id) AS cnt, '
  897.             . 'ROUND(AVG(p.mapCoordinate.latitude), 5) AS lat, '
  898.             . 'ROUND(AVG(p.mapCoordinate.longitude), 5) AS lng, '
  899.             . 'CONCAT(ROUND(p.mapCoordinate.latitude, %2$d), \',\', ROUND(p.mapCoordinate.longitude, %2$d)) AS coords, '
  900.             . 'GROUP_CONCAT(p.masseur ORDER BY p.id) AS masseurFlags',
  901.             MapClusterMinPriceDql::clusterMinPriceSelect($dqlAlias),
  902.             $precision
  903.         ));
  904.         $qb->groupBy('coords');
  905.         $specification->modify($qb, $dqlAlias);
  906.         $qb->andWhere($specification->getFilter($qb, $dqlAlias));
  907.         return $qb->getQuery()->getResult();
  908.     }
  909.     public function fetchListingByIds(ProfileIdINOrderedByINValues $specification): array
  910.     {
  911.         $ids = implode(',', $specification->getIds());
  912.         $mediaType = $this->features->crop_avatar() ? Photo::TYPE_AVATAR : Photo::TYPE_PHOTO;
  913.         $mediaIsMain = $this->features->crop_avatar() ? 0 : 1;
  914.         $sql = "
  915.             SELECT 
  916.                 p.*, JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  917.                     as `name`, 
  918.                 JSON_UNQUOTE(JSON_EXTRACT(p.description, '$.ru')) 
  919.                     as `description`,
  920.                 (SELECT path FROM profile_media_files pmf_avatar WHERE p.id = pmf_avatar.profile_id AND pmf_avatar.type = '{$mediaType}' AND pmf_avatar.is_main = {$mediaIsMain} LIMIT 1) 
  921.                     as `avatar_path`,
  922.                 (SELECT type FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  923.                     as `adboard_placement_type`,
  924.                 (SELECT position FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  925.                     as `adboard_placement_position`,
  926.                 c.id 
  927.                     as `city_id`, 
  928.                 JSON_UNQUOTE(JSON_EXTRACT(c.name, '$.ru')) 
  929.                     as `city_name`, 
  930.                 c.uri_identity 
  931.                     as `city_uri_identity`,
  932.                 c.country_code 
  933.                     as `city_country_code`,
  934.                 EXISTS(SELECT * FROM profile_top_placements ptp WHERE p.id = ptp.profile_id AND (NOW() BETWEEN ptp.placed_at AND ptp.expires_at))
  935.                     as `has_top_placement`,
  936.                 EXISTS(SELECT * FROM placement_hidings ph WHERE p.id = ph.profile_id AND ph.entity_type = 'profile') 
  937.                     as `has_placement_hiding`,
  938.                 (SELECT COUNT(*) FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  939.                     as `comments_count`,
  940.                 (SELECT COUNT(*) FROM profile_media_files pmf_photo WHERE p.id = pmf_photo.profile_id AND pmf_photo.type = 'photo') 
  941.                     as `photos_count`,
  942.                 (SELECT COUNT(*) FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  943.                     as `videos_count`,
  944.                 (SELECT COUNT(*) FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  945.                     as `selfies_count`,
  946.                 p.primary_station_id 
  947.             FROM profiles `p`
  948.             JOIN cities `c` ON c.id = p.city_id 
  949.             WHERE p.id IN ($ids)
  950.             ORDER BY FIELD(p.id,$ids)";
  951.         $connection = $this->getEntityManager()->getConnection();
  952.         $result = $connection->executeQuery($sql);
  953.         $profiles = $result->fetchAllAssociative();
  954.         $sql = "SELECT 
  955.                     cs.id 
  956.                         as `id`,
  957.                     JSON_UNQUOTE(JSON_EXTRACT(cs.name, '$.ru')) 
  958.                         as `name`, 
  959.                     cs.uri_identity 
  960.                         as `uriIdentity`, 
  961.                     ps.profile_id
  962.                         as `profile_id`,
  963.                     csl.name
  964.                         as `line_name`,
  965.                     csl.color
  966.                         as `line_color`,
  967.                     cs.county_id, cs.district_id
  968.                 FROM profile_stations ps
  969.                 JOIN city_stations cs ON ps.station_id = cs.id 
  970.                 LEFT JOIN city_subway_station_lines cssl ON cssl.station_id = cs.id
  971.                 LEFT JOIN city_subway_lines csl ON csl.id = cssl.line_id
  972.                 WHERE ps.profile_id IN ($ids)";
  973.         $result = $connection->executeQuery($sql);
  974.         $stations = $result->fetchAllAssociative();
  975.         $districtIds = array_unique(array_column($stations, 'district_id'));
  976.         $districts = $this->districts->ofIds($districtIds);
  977.         $sql = "SELECT 
  978.                     s.id 
  979.                         as `id`,
  980.                     JSON_UNQUOTE(JSON_EXTRACT(s.name, '$.ru')) 
  981.                         as `name`, 
  982.                     s.group 
  983.                         as `group`, 
  984.                     s.uri_identity 
  985.                         as `uriIdentity`,
  986.                     pps.profile_id
  987.                         as `profile_id`,
  988.                     pps.service_condition
  989.                         as `condition`,
  990.                     pps.extra_charge
  991.                         as `extra_charge`,
  992.                     pps.comment
  993.                         as `comment`
  994.                 FROM profile_provided_services pps
  995.                 JOIN services s ON pps.service_id = s.id 
  996.                 WHERE pps.profile_id IN ($ids)
  997.                 ORDER BY s.group ASC, s.sort ASC, s.id ASC";
  998.         $result = $connection->executeQuery($sql);
  999.         $providedServices = $result->fetchAllAssociative();
  1000.         $profileNameUris = $this->fetchCanonicalNameUris($ids);
  1001.         $result = array_map(function ($profile) use ($stations, $districts, $providedServices, $profileNameUris): ProfileListingReadModel {
  1002.             return $this->hydrateProfileRow2($profile, $stations, $districts, $providedServices, $profileNameUris);
  1003.         }, $profiles);
  1004.         return $result;
  1005.     }
  1006.     public function hydrateProfileRow2(array $row, array $stations, array $districts, array $services, array $profileNameUris = []): ProfileListingReadModel
  1007.     {
  1008.         $profile = new ProfileListingReadModel();
  1009.         $profile->id = $row['id'];
  1010.         $profile->moderationStatus = $row['moderation_status'];
  1011.         $profile->city = new CityReadModel();
  1012.         $profile->city->id = $row['city_id'];
  1013.         $profile->city->name = $row['city_name'];
  1014.         $profile->city->uriIdentity = $row['city_uri_identity'];
  1015.         $profile->city->countryCode = $row['city_country_code'];
  1016.         $profile->uriIdentity = $row['uri_identity'];
  1017.         $profile->name = $row['name'];
  1018.         $profile->canonicalNameUris = $profileNameUris[$profile->id] ?? [];
  1019.         $profile->description = $row['description'];
  1020.         $profile->phoneNumber = $row['phone_number'];
  1021.         $profile->isMasseur = (bool)$row['is_masseur'];
  1022.         $profile->approved = (bool)$row['is_approved'];
  1023.         $profile->isUltraVip = $row['adboard_placement_type'] == AdBoardPlacement::POSITION_GROUP_ULTRA_VIP;
  1024.         $profile->isVip = $row['adboard_placement_type'] == AdBoardPlacement::POSITION_GROUP_VIP;
  1025.         $profile->isStandard = false !== array_search(
  1026.                 $row['adboard_placement_type'],
  1027.                 [
  1028.                     AdBoardPlacement::POSITION_GROUP_STANDARD_APPROVED, AdBoardPlacement::POSITION_GROUP_STANDARD,
  1029.                     AdBoardPlacement::POSITION_GROUP_WITHOUT_OWNER_APPROVED, AdBoardPlacement::POSITION_GROUP_WITHOUT_OWNER
  1030.                 ]
  1031.             );
  1032.         $profile->position = $row['adboard_placement_position'];
  1033.         $profile->active = null !== $row['adboard_placement_type'] || $row['has_top_placement'];
  1034.         $profile->hidden = $row['has_placement_hiding'] == true;
  1035.         $profile->personParameters = new ProfilePersonParametersReadModel();
  1036.         $profile->personParameters->age = $row['person_age'];
  1037.         $profile->personParameters->height = $row['person_height'];
  1038.         $profile->personParameters->weight = $row['person_weight'];
  1039.         $profile->personParameters->breastSize = $row['person_breast_size'];
  1040.         $profile->personParameters->bodyType = $row['person_body_type'];
  1041.         $profile->personParameters->hairColor = $row['person_hair_color'];
  1042.         $profile->personParameters->privateHaircut = $row['person_private_haircut'];
  1043.         $profile->personParameters->nationality = $row['person_nationality'];
  1044.         $profile->personParameters->appearance = $row['person_appearance'];
  1045.         $profile->personParameters->hasTattoo = $row['person_has_tattoo'];
  1046.         $profile->personParameters->hasPiercing = $row['person_has_piercing'];
  1047.         $profile->stations = [];
  1048.         $profile->districts = [];
  1049.         $profile->counties = [];
  1050.         foreach ($stations as $station) {
  1051.             if ($profile->id !== $station['profile_id'])
  1052.                 continue;
  1053.             $profileStation = $profile->stations[$station['id']] ?? new StationReadModel($station['id'], $station['uriIdentity'], $station['name'], []);
  1054.             if (null !== $station['line_name']) {
  1055.                 $profileStation->lines[] = new StationLineReadModel($station['line_name'], $station['line_color']);
  1056.             }
  1057.             $profile->stations[$station['id']] = $profileStation;
  1058.             if (array_key_exists($station['district_id'] ?? 0, $districts) && !array_key_exists($station['district_id'], $profile->districts)) {
  1059.                 $profile->districts[$station['district_id']] = $districts[$station['district_id']];
  1060.             }
  1061.         }
  1062.         $primaryId = (int)$row['primary_station_id'];
  1063.         if (!empty($profile->stations)) {
  1064.             uasort($profile->stations, function (StationReadModel $a, StationReadModel $b) use ($primaryId) {
  1065.                 $aPrimary = $a->id === $primaryId;
  1066.                 $bPrimary = $b->id === $primaryId;
  1067.                 if ($aPrimary !== $bPrimary) {
  1068.                     return $aPrimary ? -1 : 1;
  1069.                 }
  1070.                 return strnatcasecmp($a->name, $b->name);
  1071.             });
  1072.         }
  1073.         if ($primaryId) {
  1074.             $profile->primaryStation = $profile->stations[$primaryId] ?? null;
  1075.         }
  1076.         $profile->providedServices = [];
  1077.         foreach ($services as $service) {
  1078.             if ($profile->id !== $service['profile_id'])
  1079.                 continue;
  1080.             $providedService = $profile->providedServices[$service['id']] ?? new ProvidedServiceReadModel(
  1081.                 $service['id'], $service['name'], $service['group'], $service['uriIdentity'],
  1082.                 $service['condition'], $service['extra_charge'], $service['comment']
  1083.             );
  1084.             $profile->providedServices[$service['id']] = $providedService;
  1085.         }
  1086.         $profile->selfies = $row['selfies_count'] ?? 0;
  1087.         $profile->videos = $row['videos_count'] ?? 0;
  1088.         $profile->photos = $row['photos_count'] ?? 0;
  1089.         $avatar = [
  1090.             'path' => $row['avatar_path'] ?? '',
  1091.             'type' => $this->features->crop_avatar() ? Photo::TYPE_AVATAR : Photo::TYPE_PHOTO
  1092.         ];
  1093.         if ($this->features->crop_avatar()) {
  1094.             $profile->avatar = $avatar;
  1095.         } else {
  1096.             $profile->mainPhoto = $avatar;
  1097.         }
  1098.         $profile->comments = $row['comments_count'] ?? 0;
  1099.         $profile->apartmentsPricing = new ProfileApartmentPricingReadModel();
  1100.         $profile->apartmentsPricing->oneHourPrice = $row['apartments_one_hour_price'];
  1101.         $profile->apartmentsPricing->twoHoursPrice = $row['apartments_two_hours_price'];
  1102.         $profile->apartmentsPricing->nightPrice = $row['apartments_night_price'];
  1103.         $profile->takeOutPricing = new ProfileTakeOutPricingReadModel();
  1104.         $profile->takeOutPricing->oneHourPrice = $row['take_out_one_hour_price'];
  1105.         $profile->takeOutPricing->twoHoursPrice = $row['take_out_two_hours_price'];
  1106.         $profile->takeOutPricing->nightPrice = $row['take_out_night_price'];
  1107.         $profile->takeOutPricing->locations = $row['take_out_locations'] ? array_map('intval', explode(',', $row['take_out_locations'])) : [];
  1108.         $profile->seo = $row['seo'] ? json_decode($row['seo'], true) : null;
  1109.         return $profile;
  1110.     }
  1111.     public function fetchMapProfilesByIds(ProfileIdINOrderedByINValues $specification): array
  1112.     {
  1113.         if ([] === $specification->getIds()) {
  1114.             return [];
  1115.         }
  1116.         $visibleIds = $this->listIdMatchingSpec(Spec::andX(
  1117.             Spec::in('id', $specification->getIds()),
  1118.             Spec::isNull('deletedAt'),
  1119.             $this->features->free_profiles() ? new ProfileIsNotHidden() : new ProfileIsActive(),
  1120.             $this->features->hard_moderation() ? new ProfileIsModerationPassed() : new ProfileIsNotRejected(),
  1121.         ), -1, null);
  1122.         if ([] === $visibleIds) {
  1123.             return [];
  1124.         }
  1125.         $ids = implode(',', $visibleIds);
  1126.         $mediaType = $this->features->crop_avatar() ? Photo::TYPE_AVATAR : Photo::TYPE_PHOTO;
  1127.         $mediaIsMain = $this->features->crop_avatar() ? 0 : 1;
  1128.         $sql = "
  1129.             SELECT 
  1130.                 p.id, p.uri_identity, p.map_latitude, p.map_longitude, p.phone_number, p.is_masseur, p.is_approved,
  1131.                 p.person_age, p.person_breast_size, p.person_height, p.person_weight, pap.type as placement_type, p.primary_station_id,
  1132.                 JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  1133.                     as `name`,
  1134.                 (SELECT path FROM profile_media_files pmf_avatar WHERE p.id = pmf_avatar.profile_id AND pmf_avatar.type = '{$mediaType}' AND pmf_avatar.is_main = {$mediaIsMain} LIMIT 1) 
  1135.                     as `avatar_path`,
  1136.                 p.apartments_one_hour_price, p.apartments_two_hours_price, p.apartments_night_price, p.take_out_one_hour_price, p.take_out_two_hours_price, p.take_out_night_price,
  1137.                 GROUP_CONCAT(ps.station_id) as `stations`,
  1138.                 GROUP_CONCAT(pps.service_id) as `services`,
  1139.                 EXISTS(SELECT * FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  1140.                     as `has_comments`,
  1141.                 EXISTS(SELECT * FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  1142.                     as `has_videos`,
  1143.                 EXISTS(SELECT * FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  1144.                     as `has_selfies`,
  1145.                 EXISTS(SELECT * FROM profile_top_placements ptp WHERE p.id = ptp.profile_id AND (NOW() BETWEEN ptp.placed_at AND ptp.expires_at))
  1146.                     as `has_top_placement`
  1147.             FROM profiles `p`
  1148.             LEFT JOIN profile_stations ps ON ps.profile_id = p.id
  1149.             LEFT JOIN profile_provided_services pps ON pps.profile_id = p.id
  1150.             LEFT JOIN profile_adboard_placements pap ON pap.profile_id = p.id
  1151.             WHERE p.id IN ($ids)
  1152.             GROUP BY p.id
  1153.             "; // AND p.map_latitude IS NOT NULL AND p.map_longitude IS NOT NULL; ORDER BY FIELD(p.id,$ids)
  1154.         $result = $this->getEntityManager()->getConnection()->executeQuery($sql);
  1155.         $profiles = $result->fetchAllAssociative();
  1156.         $result = array_map(function ($profile): ProfileMapReadModel {
  1157.             return $this->hydrateMapProfileRow($profile);
  1158.         }, $profiles);
  1159.         return $result;
  1160.     }
  1161.     public function hydrateMapProfileRow(array $row): ProfileMapReadModel
  1162.     {
  1163.         $profile = new ProfileMapReadModel();
  1164.         $profile->id = $row['id'];
  1165.         $profile->uriIdentity = $row['uri_identity'];
  1166.         $profile->name = $row['name'];
  1167.         $profile->phoneNumber = $row['phone_number'];
  1168.         $profile->avatar = ['path' => $row['avatar_path'] ?? '', 'type' => $this->features->crop_avatar() ? Photo::TYPE_AVATAR : Photo::TYPE_PHOTO];
  1169.         $profile->mapLatitude = $row['map_latitude'];
  1170.         $profile->mapLongitude = $row['map_longitude'];
  1171.         $profile->age = $row['person_age'];
  1172.         $profile->breastSize = $row['person_breast_size'];
  1173.         $profile->height = $row['person_height'];
  1174.         $profile->weight = $row['person_weight'];
  1175.         $profile->isMasseur = $row['is_masseur'];
  1176.         $profile->isApproved = $row['is_approved'];
  1177.         $profile->hasComments = $row['has_comments'];
  1178.         $profile->hasSelfies = $row['has_selfies'];
  1179.         $profile->hasVideos = $row['has_videos'];
  1180.         $profile->apartmentOneHourPrice = $row['apartments_one_hour_price'];
  1181.         $profile->apartmentTwoHoursPrice = $row['apartments_two_hours_price'];
  1182.         $profile->apartmentNightPrice = $row['apartments_night_price'];
  1183.         $profile->takeOutOneHourPrice = $row['take_out_one_hour_price'];
  1184.         $profile->takeOutTwoHoursPrice = $row['take_out_two_hours_price'];
  1185.         $profile->takeOutNightPrice = $row['take_out_night_price'];
  1186.         $profile->station = $row['primary_station_id'] ?? ($row['stations'] ? explode(',', $row['stations'])[0] : null);
  1187.         $profile->services = $row['services'] ? array_unique(explode(',', $row['services'])) : [];
  1188.         $profile->isPaid = $row['placement_type'] >= AdBoardPlacement::POSITION_GROUP_STANDARD || (bool) $row['has_top_placement'];
  1189. //        $prices = [ $row['apartments_one_hour_price'], $row['apartments_two_hours_price'], $row['apartments_night_price'],
  1190. //            $row['take_out_one_hour_price'], $row['take_out_two_hours_price'], $row['take_out_night_price'] ];
  1191. //        $prices = array_filter($prices, function($item) {
  1192. //            return $item != null;
  1193. //        });
  1194. //        $profile->price = count($prices) ? min($prices) : null;
  1195.         return $profile;
  1196.     }
  1197.     public function fetchAccountProfileListByIds(ProfileIdINOrderedByINValues $specification): array
  1198.     {
  1199.         $ids = implode(',', $specification->getIds());
  1200.         $mediaType = $this->features->crop_avatar() ? Photo::TYPE_AVATAR : Photo::TYPE_PHOTO;
  1201.         $mediaIsMain = $this->features->crop_avatar() ? 0 : 1;
  1202.         $sql = "
  1203.             SELECT 
  1204.                 p.*, JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  1205.                     as `name`, 
  1206.                 JSON_UNQUOTE(JSON_EXTRACT(p.description, '$.ru')) 
  1207.                     as `description`,
  1208.                 (SELECT path FROM profile_media_files pmf_avatar WHERE p.id = pmf_avatar.profile_id AND pmf_avatar.type = '{$mediaType}' AND pmf_avatar.is_main = {$mediaIsMain} LIMIT 1) 
  1209.                     as `avatar_path`,
  1210.                 (SELECT type FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  1211.                     as `adboard_placement_type`,
  1212.                 c.id 
  1213.                     as `city_id`, 
  1214.                 JSON_UNQUOTE(JSON_EXTRACT(c.name, '$.ru')) 
  1215.                     as `city_name`, 
  1216.                 c.uri_identity 
  1217.                     as `city_uri_identity`,
  1218.                 c.country_code 
  1219.                     as `city_country_code`,
  1220.                 EXISTS(SELECT * FROM profile_top_placements ptp WHERE p.id = ptp.profile_id AND (NOW() BETWEEN ptp.placed_at AND ptp.expires_at))
  1221.                     as `has_top_placement`,
  1222.                 EXISTS(SELECT * FROM placement_hidings ph WHERE p.id = ph.profile_id AND ph.entity_type = 'profile') 
  1223.                     as `has_placement_hiding`,
  1224.                 (SELECT COUNT(*) FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  1225.                     as `comments_count`,
  1226.                 (SELECT COUNT(*) FROM profile_media_files pmf_photo WHERE p.id = pmf_photo.profile_id AND pmf_photo.type = 'photo') 
  1227.                     as `photos_count`,
  1228.                 (SELECT COUNT(*) FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  1229.                     as `videos_count`,
  1230.                 (SELECT COUNT(*) FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  1231.                     as `selfies_count`,
  1232.                 p.primary_station_id 
  1233.             FROM profiles `p`
  1234.             JOIN cities `c` ON c.id = p.city_id 
  1235.             WHERE p.id IN ($ids)
  1236.             ORDER BY FIELD(p.id,$ids)";
  1237.         $connection = $this->getEntityManager()->getConnection();
  1238.         $result = $connection->executeQuery($sql);
  1239.         $profiles = $result->fetchAllAssociative();
  1240.         $sql = "SELECT 
  1241.                     JSON_UNQUOTE(JSON_EXTRACT(cs.name, '$.ru')) 
  1242.                         as `name`, 
  1243.                     cs.uri_identity 
  1244.                         as `uriIdentity`, 
  1245.                     ps.profile_id
  1246.                         as `profile_id`,
  1247.                     cs.district_id, cs.county_id
  1248.                 FROM profile_stations ps
  1249.                 JOIN city_stations cs ON ps.station_id = cs.id                 
  1250.                 WHERE ps.profile_id IN ($ids)";
  1251.         $result = $connection->executeQuery($sql);
  1252.         $stations = $result->fetchAllAssociative();
  1253.         $districtIds = array_unique(array_column($stations, 'district_id'));
  1254.         $districts = $this->districts->ofIds($districtIds);
  1255.         $sql = "SELECT 
  1256.                     s.id 
  1257.                         as `id`,
  1258.                     JSON_UNQUOTE(JSON_EXTRACT(s.name, '$.ru')) 
  1259.                         as `name`, 
  1260.                     s.group 
  1261.                         as `group`, 
  1262.                     s.uri_identity 
  1263.                         as `uriIdentity`,
  1264.                     pps.profile_id
  1265.                         as `profile_id`,
  1266.                     pps.service_condition
  1267.                         as `condition`,
  1268.                     pps.extra_charge
  1269.                         as `extra_charge`,
  1270.                     pps.comment
  1271.                         as `comment`
  1272.                 FROM profile_provided_services pps
  1273.                 JOIN services s ON pps.service_id = s.id 
  1274.                 WHERE pps.profile_id IN ($ids)
  1275.                 ORDER BY s.group ASC, s.sort ASC, s.id ASC";
  1276.         $result = $connection->executeQuery($sql);
  1277.         $providedServices = $result->fetchAllAssociative();
  1278.         $profileNameUris = $this->fetchCanonicalNameUris($ids);
  1279.         $result = array_map(function ($profile) use ($stations, $districts, $providedServices, $profileNameUris): ProfileListingReadModel {
  1280.             return $this->hydrateProfileRow2($profile, $stations, $districts, $providedServices, $profileNameUris);
  1281.         }, $profiles);
  1282.         return $result;
  1283.     }
  1284.     private function fetchCanonicalNameUris(string $ids): array
  1285.     {
  1286.         $rows = $this->getEntityManager()->getConnection()->fetchAllAssociative(
  1287.             "SELECT ppn.profile_id, pn.uri_identity
  1288.              FROM profile_profile_names ppn
  1289.              JOIN profile_names pn ON pn.id = ppn.profile_name_id
  1290.              WHERE ppn.profile_id IN ($ids)
  1291.              ORDER BY pn.canonical_name ASC"
  1292.         );
  1293.         $result = [];
  1294.         foreach ($rows as $row) {
  1295.             $profileId = (int)$row['profile_id'];
  1296.             $result[$profileId] ??= [];
  1297.             $result[$profileId][] = $row['uri_identity'];
  1298.         }
  1299.         return $result;
  1300.     }
  1301.     public function getCommentedProfilesPaged(User $owner): ORMQueryResult
  1302.     {
  1303.         $qb = $this->createQueryBuilder('profile')
  1304.             ->join('profile.comments', 'comment')
  1305.             ->andWhere('profile.owner = :owner')
  1306.             ->setParameter('owner', $owner)
  1307.             ->orderBy('comment.createdAt', 'DESC');
  1308.         return new ORMQueryResult($qb);
  1309.     }
  1310.     /**
  1311.      * @return ProfilePlacementPriceDetailReadModel[]
  1312.      */
  1313.     public function fetchOfOwnerPlacedPriceDetails(User $owner): array
  1314.     {
  1315.         $sql = "
  1316.             SELECT 
  1317.                 p.id, p.is_approved, psp.price_amount
  1318.             FROM profiles `p`
  1319.             JOIN profile_adboard_placements pap ON pap.profile_id = p.id AND pap.placement_price_id IS NOT NULL
  1320.             JOIN paid_service_prices psp ON pap.placement_price_id = psp.id
  1321.             WHERE p.user_id = {$owner->getId()}
  1322.         ";
  1323.         $result = $this->getEntityManager()->getConnection()->executeQuery($sql);
  1324.         $profiles = $result->fetchAllAssociative();
  1325.         return array_map(function (array $row): ProfilePlacementPriceDetailReadModel {
  1326.             return new ProfilePlacementPriceDetailReadModel(
  1327.                 $row['id'], $row['is_approved'], $row['price_amount'] / 24
  1328.             );
  1329.         }, $profiles);
  1330.     }
  1331.     /**
  1332.      * @return ProfilePlacementHidingDetailReadModel[]
  1333.      */
  1334.     public function fetchOfOwnerHiddenDetails(User $owner): array
  1335.     {
  1336.         $sql = "
  1337.             SELECT 
  1338.                 p.id, p.is_approved
  1339.             FROM profiles `p`
  1340.             JOIN placement_hidings ph ON ph.profile_id = p.id
  1341.             WHERE p.user_id = {$owner->getId()}
  1342.         ";
  1343.         $result = $this->getEntityManager()->getConnection()->executeQuery($sql);
  1344.         $profiles = $result->fetchAllAssociative();
  1345.         return array_map(function (array $row): ProfilePlacementHidingDetailReadModel {
  1346.             return new ProfilePlacementHidingDetailReadModel(
  1347.                 $row['id'], $row['is_approved'], true
  1348.             );
  1349.         }, $profiles);
  1350.     }
  1351.     protected function modifyListingQueryBuilder(QueryBuilder $qb, string $alias): void
  1352.     {
  1353.         $qb
  1354.             ->addSelect('city')
  1355.             ->addSelect('station')
  1356.             ->addSelect('photo')
  1357.             ->addSelect('video')
  1358.             ->addSelect('comment')
  1359.             ->addSelect('avatar')
  1360.             ->join(sprintf('%s.city', $alias), 'city');
  1361.         if (!in_array('station', $qb->getAllAliases()))
  1362.             $qb->leftJoin(sprintf('%s.stations', $alias), 'station');
  1363.         if (!in_array('photo', $qb->getAllAliases()))
  1364.             $qb->leftJoin(sprintf('%s.photos', $alias), 'photo');
  1365.         if (!in_array('video', $qb->getAllAliases()))
  1366.             $qb->leftJoin(sprintf('%s.videos', $alias), 'video');
  1367.         if (!in_array('avatar', $qb->getAllAliases()))
  1368.             $qb->leftJoin(sprintf('%s.avatar', $alias), 'avatar');
  1369.         if (!in_array('comment', $qb->getAllAliases()))
  1370.             $qb->leftJoin(sprintf('%s.comments', $alias), 'comment');
  1371.         $this->addFemaleGenderFilterToQb($qb, $alias);
  1372.         //TODO убрать, если все ок
  1373.         //$this->excludeHavingPlacementHiding($qb, $alias);
  1374.         if (!in_array('profile_adboard_placement', $qb->getAllAliases())) {
  1375.             $qb
  1376.                 ->leftJoin(sprintf('%s.adBoardPlacement', $alias), 'profile_adboard_placement');
  1377.         }
  1378.         $qb->addSelect('profile_adboard_placement');
  1379.         if (!in_array('profile_top_placement', $qb->getAllAliases())) {
  1380.             $qb
  1381.                 ->leftJoin(sprintf('%s.topPlacements', $alias), 'profile_top_placement');
  1382.         }
  1383.         $qb->addSelect('profile_top_placement');
  1384.         //if($this->features->free_profiles()) {
  1385.         if (!in_array('placement_hiding', $qb->getAllAliases())) {
  1386.             $qb
  1387.                 ->leftJoin(sprintf('%s.placementHiding', $alias), 'placement_hiding');
  1388.         }
  1389.         $qb->addSelect('placement_hiding');
  1390.         //}
  1391.     }
  1392.     protected function addActiveFilterToQb(QueryBuilder $qb, string $dqlAlias)
  1393.     {
  1394.         if (!in_array('profile_adboard_placement', $qb->getAllAliases())) {
  1395.             $qb
  1396.                 ->join(sprintf('%s.adBoardPlacement', $dqlAlias), 'profile_adboard_placement');
  1397.         }
  1398.     }
  1399.     private function excludeHavingPlacementHiding(QueryBuilder $qb, $alias): void
  1400.     {
  1401.         if ($this->features->free_profiles()) {
  1402. //            if (!in_array('placement_hiding', $qb->getAllAliases())) {
  1403. //                $qb
  1404. //                    ->leftJoin(sprintf('%s.placementHiding', $alias), 'placement_hiding')
  1405. //                    ->andWhere(sprintf('placement_hiding IS NULL'))
  1406. //                ;
  1407. //        }
  1408.             $sub = new QueryBuilder($qb->getEntityManager());
  1409.             $sub->select("exclude_hidden_placement_hiding");
  1410.             $sub->from($qb->getEntityManager()->getClassMetadata(PlacementHiding::class)->name, "exclude_hidden_placement_hiding");
  1411.             $sub->andWhere(sprintf('exclude_hidden_placement_hiding.profile = %s', $alias));
  1412.             $qb->andWhere($qb->expr()->not($qb->expr()->exists($sub->getDQL())));
  1413.         }
  1414.     }
  1415. }