src/Repository/ProfileRepository.php line 940

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