src/Repository/ProfileRepository.php line 1035

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