src/Repository/ProfileRepository.php line 118

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\Location\City;
  9. use App\Entity\Location\MapCoordinate;
  10. use App\Entity\Profile\Genders;
  11. use App\Entity\Profile\Photo;
  12. use App\Entity\Profile\Profile;
  13. use App\Entity\Sales\Profile\AdBoardPlacement;
  14. use App\Entity\Sales\Profile\AdBoardPlacementType;
  15. use App\Entity\Sales\Profile\PlacementHiding;
  16. use App\Entity\User;
  17. use App\Repository\ReadModel\CityReadModel;
  18. use App\Repository\ReadModel\ProfileApartmentPricingReadModel;
  19. use App\Repository\ReadModel\ProfileListingReadModel;
  20. use App\Repository\ReadModel\ProfileMapReadModel;
  21. use App\Repository\ReadModel\ProfilePersonParametersReadModel;
  22. use App\Repository\ReadModel\ProfilePlacementHidingDetailReadModel;
  23. use App\Repository\ReadModel\ProfilePlacementPriceDetailReadModel;
  24. use App\Repository\ReadModel\ProfileTakeOutPricingReadModel;
  25. use App\Repository\ReadModel\ProvidedServiceReadModel;
  26. use App\Repository\ReadModel\StationLineReadModel;
  27. use App\Repository\ReadModel\StationReadModel;
  28. use App\Service\Features;
  29. use App\Specification\Profile\ProfileIdINOrderedByINValues;
  30. use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
  31. use Doctrine\ORM\AbstractQuery;
  32. use Doctrine\Persistence\ManagerRegistry;
  33. use Doctrine\DBAL\Statement;
  34. use Doctrine\ORM\QueryBuilder;
  35. use Happyr\DoctrineSpecification\Filter\Filter;
  36. use Happyr\DoctrineSpecification\Query\QueryModifier;
  37. use Porpaginas\Doctrine\ORM\ORMQueryResult;
  38. class ProfileRepository extends ServiceEntityRepository
  39. {
  40.     use SpecificationTrait;
  41.     use EntityIteratorTrait;
  42.     private Features $features;
  43.     public function __construct(ManagerRegistry $registryFeatures $features)
  44.     {
  45.         parent::__construct($registryProfile::class);
  46.         $this->features $features;
  47.     }
  48.     /**
  49.      * Возвращает итератор по данным, необходимым для генерации файлов sitemap, в виде массивов с
  50.      * следующими ключами:
  51.      *  - id
  52.      *  - uri
  53.      *  - updatedAt
  54.      *  - city_uri
  55.      *
  56.      * @return iterable<array{id: int, uri: string, updatedAt: \DateTimeImmutable, city_uri: string}>
  57.      */
  58.     public function sitemapItemsIterator(): iterable
  59.     {
  60.         $qb $this->createQueryBuilder('profile')
  61.             ->select('profile.id, profile.uriIdentity AS uri, profile.updatedAt, city.uriIdentity AS city_uri')
  62.             ->join('profile.city''city')
  63.             ->andWhere('profile.deletedAt IS NULL');
  64.         $this->addModerationFilterToQb($qb'profile');
  65.         return $qb->getQuery()->toIterable([], AbstractQuery::HYDRATE_ARRAY);
  66.     }
  67.     protected function addModerationFilterToQb(QueryBuilder $qbstring $dqlAlias): void
  68.     {
  69.         if ($this->features->hard_moderation()) {
  70.             $qb->leftJoin(sprintf('%s.owner'$dqlAlias), 'owner');
  71.             $qb->andWhere(
  72.                 $qb->expr()->orX(
  73.                     sprintf('%s.moderationStatus = :status_passed'$dqlAlias),
  74.                     $qb->expr()->andX(
  75.                         sprintf('%s.moderationStatus = :status_waiting'$dqlAlias),
  76.                         'owner.trusted = true'
  77.                     )
  78.                 )
  79.             );
  80.             $qb->setParameter('status_passed'Profile::MODERATION_STATUS_APPROVED);
  81.             $qb->setParameter('status_waiting'Profile::MODERATION_STATUS_WAITING);
  82.         } else {
  83.             $qb->andWhere(sprintf('%s.moderationStatus IN (:statuses)'$dqlAlias));
  84.             $qb->setParameter('statuses', [Profile::MODERATION_STATUS_NOT_PASSEDProfile::MODERATION_STATUS_WAITINGProfile::MODERATION_STATUS_APPROVED]);
  85.         }
  86.     }
  87.     public function ofUriIdentityWithinCity(string $uriIdentityCity $city): ?Profile
  88.     {
  89.         return $this->findOneBy([
  90.             'uriIdentity' => $uriIdentity,
  91.             'city' => $city,
  92.         ]);
  93.     }
  94.     /**
  95.      * Метод проверки уникальности анкет по URI не должен использовать никаких фильтров, кроме URI и города,
  96.      * поэтому QueryBuilder не используется
  97.      * @see https://redminez.net/issues/27310
  98.      */
  99.     public function isUniqueUriIdentityExistWithinCity(string $uriIdentityCity $city): bool
  100.     {
  101.         $connection $this->_em->getConnection();
  102.         $stmt $connection->executeQuery('SELECT COUNT(id) FROM profiles WHERE uri_identity = ? AND city_id = ?', [$uriIdentity$city->getId()]);
  103.         $count $stmt->fetchOne();
  104.         return $count 0;
  105.     }
  106.     public function countByCity(): array
  107.     {
  108.         $qb $this->createQueryBuilder('profile')
  109.             ->select('IDENTITY(profile.city), COUNT(profile.id)')
  110.             ->groupBy('profile.city');
  111.         $this->addFemaleGenderFilterToQb($qb'profile');
  112.         $this->addModerationFilterToQb($qb'profile');
  113.         //$this->excludeHavingPlacementHiding($qb, 'profile');
  114.         $this->havingAdBoardPlacement($qb'profile');
  115.         $query $qb->getQuery()
  116.             ->useResultCache(true)
  117.             ->setResultCacheLifetime(120);
  118.         $rawResult $query->getScalarResult();
  119.         $indexedResult = [];
  120.         foreach ($rawResult as $row) {
  121.             $indexedResult[$row[1]] = $row[2];
  122.         }
  123.         return $indexedResult;
  124.     }
  125.     protected function addFemaleGenderFilterToQb(QueryBuilder $qbstring $alias): void
  126.     {
  127.         $this->addGenderFilterToQb($qb$alias, [Genders::FEMALE]);
  128.     }
  129.     protected function addGenderFilterToQb(QueryBuilder $qbstring $alias, array $genders = [Genders::FEMALE]): void
  130.     {
  131.         $qb->andWhere(sprintf('%s.personParameters.gender IN (:genders)'$alias));
  132.         $qb->setParameter('genders'$genders);
  133.     }
  134.     private function havingAdBoardPlacement(QueryBuilder $qbstring $alias): void
  135.     {
  136.         $qb->join(sprintf('%s.adBoardPlacement'$alias), 'adboard_placement');
  137.     }
  138.     public function countByStations(): array
  139.     {
  140.         $qb $this->createQueryBuilder('profiles')
  141.             ->select('stations.id, COUNT(profiles.id) as cnt')
  142.             ->join('profiles.stations''stations')
  143.             //это условие сильно затормжаживает запрос, но оно и не нужно при условии, что чужих(от других городов) станций у анкеты нет
  144.             //->where('profiles.city = stations.city')
  145.             ->groupBy('stations.id');
  146.         $this->addFemaleGenderFilterToQb($qb'profiles');
  147.         $this->addModerationFilterToQb($qb'profiles');
  148.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  149.         $this->havingAdBoardPlacement($qb'profiles');
  150.         $query $qb->getQuery()
  151.             ->useResultCache(true)
  152.             ->setResultCacheLifetime(120);
  153.         $rawResult $query->getScalarResult();
  154.         $indexedResult = [];
  155.         foreach ($rawResult as $row) {
  156.             $indexedResult[$row['id']] = $row['cnt'];
  157.         }
  158.         return $indexedResult;
  159.     }
  160.     public function countByDistricts(): array
  161.     {
  162.         $qb $this->createQueryBuilder('profiles')
  163.             ->select('districts.id, COUNT(profiles.id) as cnt')
  164.             ->join('profiles.stations''stations')
  165.             ->join('stations.district''districts')
  166.             ->groupBy('districts.id');
  167.         $this->addFemaleGenderFilterToQb($qb'profiles');
  168.         $this->addModerationFilterToQb($qb'profiles');
  169.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  170.         $this->havingAdBoardPlacement($qb'profiles');
  171.         $query $qb->getQuery()
  172.             ->useResultCache(true)
  173.             ->setResultCacheLifetime(120);
  174.         $rawResult $query->getScalarResult();
  175.         $indexedResult = [];
  176.         foreach ($rawResult as $row) {
  177.             $indexedResult[$row['id']] = $row['cnt'];
  178.         }
  179.         return $indexedResult;
  180.     }
  181.     public function countByCounties(): array
  182.     {
  183.         $qb $this->createQueryBuilder('profiles')
  184.             ->select('counties.id, COUNT(profiles.id) as cnt')
  185.             ->join('profiles.stations''stations')
  186.             ->join('stations.district''districts')
  187.             ->join('districts.county''counties')
  188.             ->groupBy('counties.id');
  189.         $this->addFemaleGenderFilterToQb($qb'profiles');
  190.         $this->addModerationFilterToQb($qb'profiles');
  191.         //$this->excludeHavingPlacementHiding($qb, 'profiles');
  192.         $this->havingAdBoardPlacement($qb'profiles');
  193.         $query $qb->getQuery()
  194.             ->useResultCache(true)
  195.             ->setResultCacheLifetime(120);
  196.         $rawResult $query->getScalarResult();
  197.         $indexedResult = [];
  198.         foreach ($rawResult as $row) {
  199.             $indexedResult[$row['id']] = $row['cnt'];
  200.         }
  201.         return $indexedResult;
  202.     }
  203.     /**
  204.      * @param array|int[] $ids
  205.      * @return Profile[]
  206.      */
  207.     public function findByIds(array $ids): array
  208.     {
  209.         return $this->createQueryBuilder('profile')
  210.             ->andWhere('profile.id IN (:ids)')
  211.             ->setParameter('ids'$ids)
  212.             ->orderBy('FIELD(profile.id,:ids2)')
  213.             ->setParameter('ids2'$ids)
  214.             ->getQuery()
  215.             ->getResult();
  216.     }
  217.     public function findByIdsIterate(array $ids): iterable
  218.     {
  219.         $qb $this->createQueryBuilder('profile')
  220.             ->andWhere('profile.id IN (:ids)')
  221.             ->setParameter('ids'$ids)
  222.             ->orderBy('FIELD(profile.id,:ids2)')
  223.             ->setParameter('ids2'$ids);
  224.         return $this->iterateQueryBuilder($qb);
  225.     }
  226.     /**
  227.      * Список анкет указанного типа (массажистки или нет), привязанных к аккаунту
  228.      */
  229.     public function ofOwnerAndTypePaged(User $ownerbool $masseurs): ORMQueryResult
  230.     {
  231.         $qb $this->createQueryBuilder('profile')
  232.             ->andWhere('profile.owner = :owner')
  233.             ->setParameter('owner'$owner)
  234.             ->andWhere('profile.masseur = :is_masseur')
  235.             ->setParameter('is_masseur'$masseurs);
  236.         return new ORMQueryResult($qb);
  237.     }
  238.     /**
  239.      * Список активных анкет, привязанных к аккаунту
  240.      */
  241.     public function activeAndOwnedBy(User $owner): ORMQueryResult
  242.     {
  243.         $qb $this->createQueryBuilder('profile')
  244.             ->join('profile.adBoardPlacement''profile_adboard_placement')
  245.             ->andWhere('profile.owner = :owner')
  246.             ->setParameter('owner'$owner);
  247.         return new ORMQueryResult($qb);
  248.     }
  249.     /**
  250.      * Список активных или скрытых анкет, привязанных к аккаунту
  251.      *
  252.      * @return Profile[]|ORMQueryResult
  253.      */
  254.     public function activeOrHiddenAndOwnedBy(User $owner): ORMQueryResult
  255.     {
  256.         $qb $this->createQueryBuilder('profile')
  257.             ->join('profile.adBoardPlacement''profile_adboard_placement')
  258. //            ->leftJoin('profile.placementHiding', 'placement_hiding')
  259. //            ->andWhere('profile_adboard_placement IS NOT NULL OR placement_hiding IS NOT NULL')
  260.             ->andWhere('profile.owner = :owner')
  261.             ->setParameter('owner'$owner);
  262. //        return $this->iterateQueryBuilder($qb);
  263.         return new ORMQueryResult($qb);
  264.     }
  265.     public function countFreeUnapprovedLimited(): int
  266.     {
  267.         $qb $this->createQueryBuilder('profile')
  268.             ->select('count(profile)')
  269.             ->join('profile.adBoardPlacement''placement')
  270.             ->andWhere('placement.type = :placement_type')
  271.             ->setParameter('placement_type'AdBoardPlacementType::FREE)
  272.             ->leftJoin('profile.placementHiding''hiding')
  273.             ->andWhere('hiding IS NULL')
  274.             ->andWhere('profile.approved = false');
  275.         return (int)$qb->getQuery()->getSingleScalarResult();
  276.     }
  277.     public function iterateFreeUnapprovedLimited(int $limit): iterable
  278.     {
  279.         $qb $this->createQueryBuilder('profile')
  280.             ->join('profile.adBoardPlacement''placement')
  281.             ->andWhere('placement.type = :placement_type')
  282.             ->setParameter('placement_type'AdBoardPlacementType::FREE)
  283.             ->leftJoin('profile.placementHiding''hiding')
  284.             ->andWhere('hiding IS NULL')
  285.             ->andWhere('profile.approved = false')
  286.             ->setMaxResults($limit);
  287.         return $this->iterateQueryBuilder($qb);
  288.     }
  289.     /**
  290.      * Число активных анкет, привязанных к аккаунту
  291.      */
  292.     public function countActiveOfOwner(User $owner, ?bool $isMasseur false): int
  293.     {
  294.         $qb $this->createQueryBuilder('profile')
  295.             ->select('COUNT(profile.id)')
  296.             ->join('profile.adBoardPlacement''profile_adboard_placement')
  297.             ->andWhere('profile.owner = :owner')
  298.             ->setParameter('owner'$owner);
  299.         if ($this->features->hard_moderation()) {
  300.             $qb->leftJoin('profile.owner''owner');
  301.             $qb->andWhere(
  302.                 $qb->expr()->orX(
  303.                     'profile.moderationStatus = :status_passed',
  304.                     $qb->expr()->andX(
  305.                         'profile.moderationStatus = :status_waiting',
  306.                         'owner.trusted = true'
  307.                     )
  308.                 )
  309.             );
  310.             $qb->setParameter('status_passed'Profile::MODERATION_STATUS_APPROVED);
  311.             $qb->setParameter('status_waiting'Profile::MODERATION_STATUS_WAITING);
  312.         } else {
  313.             $qb->andWhere('profile.moderationStatus IN (:statuses)')
  314.                 ->setParameter('statuses', [Profile::MODERATION_STATUS_NOT_PASSEDProfile::MODERATION_STATUS_WAITINGProfile::MODERATION_STATUS_APPROVED]);
  315.         }
  316.         if (null !== $isMasseur) {
  317.             $qb->andWhere('profile.masseur = :is_masseur')
  318.                 ->setParameter('is_masseur'$isMasseur);
  319.         }
  320.         return (int)$qb->getQuery()->getSingleScalarResult();
  321.     }
  322.     /**
  323.      * Число всех анкет, привязанных к аккаунту
  324.      */
  325.     public function countAllOfOwnerNotDeleted(User $owner, ?bool $isMasseur false): int
  326.     {
  327.         $qb $this->createQueryBuilder('profile')
  328.             ->select('COUNT(profile.id)')
  329.             ->andWhere('profile.owner = :owner')
  330.             ->setParameter('owner'$owner)
  331.             //потому что используется в т.ч. на тех страницах, где отключен фильтр вывода "только неудаленных"
  332.             ->andWhere('profile.deletedAt IS NULL');
  333.         if (null !== $isMasseur) {
  334.             $qb->andWhere('profile.masseur = :is_masseur')
  335.                 ->setParameter('is_masseur'$isMasseur);
  336.         }
  337.         return (int)$qb->getQuery()->getSingleScalarResult();
  338.     }
  339.     public function getTimezonesListByUser(User $owner): array
  340.     {
  341.         $q $this->_em->createQuery(sprintf("
  342.                 SELECT c
  343.                 FROM %s c
  344.                 WHERE c.id IN (
  345.                     SELECT DISTINCT(c2.id) 
  346.                     FROM %s p
  347.                     JOIN p.city c2
  348.                     WHERE p.owner = :user
  349.                 )
  350.             "$this->_em->getClassMetadata(City::class)->name$this->_em->getClassMetadata(Profile::class)->name))
  351.             ->setParameter('user'$owner);
  352.         return $q->getResult();
  353.     }
  354.     /**
  355.      * Список анкет, привязанных к аккаунту
  356.      *
  357.      * @return Profile[]
  358.      */
  359.     public function ofOwner(User $owner): array
  360.     {
  361.         $qb $this->createQueryBuilder('profile')
  362.             ->andWhere('profile.owner = :owner')
  363.             ->setParameter('owner'$owner);
  364.         return $qb->getQuery()->getResult();
  365.     }
  366.     public function ofOwnerPaged(User $owner, array $genders = [Genders::FEMALE]): ORMQueryResult
  367.     {
  368.         $qb $this->createQueryBuilder('profile')
  369.             ->andWhere('profile.owner = :owner')
  370.             ->setParameter('owner'$owner)
  371.             ->andWhere('profile.personParameters.gender IN (:genders)')
  372.             ->setParameter('genders'$genders);
  373.         return new ORMQueryResult($qb);
  374.     }
  375.     public function ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterIterateAll(User $ownerstring $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur null): \Generator
  376.     {
  377.         $query $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner$placementTypeFilter$nameFilter$isMasseur)->getQuery();
  378.         foreach ($query->iterate() as $row) {
  379.             yield $row[0];
  380.         }
  381.     }
  382.     private function queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $ownerstring $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur null): QueryBuilder
  383.     {
  384.         $qb $this->createQueryBuilder('profile')
  385.             ->andWhere('profile.owner = :owner')
  386.             ->setParameter('owner'$owner);
  387.         switch ($placementTypeFilter) {
  388.             case 'paid':
  389.                 $qb->join('profile.adBoardPlacement''placement')
  390.                     ->andWhere('placement.type != :placement_type')
  391.                     ->setParameter('placement_type'AdBoardPlacementType::FREE);
  392.                 break;
  393.             case 'free':
  394.                 $qb->join('profile.adBoardPlacement''placement')
  395.                     ->andWhere('placement.type = :placement_type')
  396.                     ->setParameter('placement_type'AdBoardPlacementType::FREE);
  397.                 break;
  398.             case 'ultra-vip':
  399.                 $qb->join('profile.adBoardPlacement''placement')
  400.                     ->andWhere('placement.type = :placement_type')
  401.                     ->setParameter('placement_type'AdBoardPlacementType::ULTRA_VIP);
  402.                 break;
  403.             case 'vip':
  404.                 $qb->join('profile.adBoardPlacement''placement')
  405.                     ->andWhere('placement.type = :placement_type')
  406.                     ->setParameter('placement_type'AdBoardPlacementType::VIP);
  407.                 break;
  408.             case 'standard':
  409.                 $qb->join('profile.adBoardPlacement''placement')
  410.                     ->andWhere('placement.type = :placement_type')
  411.                     ->setParameter('placement_type'AdBoardPlacementType::STANDARD);
  412.                 break;
  413.             case 'hidden':
  414.                 $qb->join('profile.placementHiding''placement_hiding');
  415.                 break;
  416.             case 'all':
  417.             default:
  418.                 break;
  419.         }
  420.         if ($nameFilter) {
  421.             $nameExpr $qb->expr()->orX(
  422.                 'LOWER(JSON_UNQUOTE(JSON_EXTRACT(profile.name, :jsonPath))) LIKE :name_filter',
  423.                 \sprintf("REGEXP_REPLACE(profile.phoneNumber, '-| ', '') LIKE :name_filter"),
  424.                 'LOWER(profile.phoneNumber) LIKE :name_filter',
  425.                 \sprintf("REGEXP_REPLACE(profile.phoneNumber, '\+7', '8') LIKE :name_filter"),
  426.             );
  427.             $qb->setParameter('jsonPath''$.ru');
  428.             $qb->setParameter('name_filter''%' addcslashes(mb_strtolower(str_replace(['('')'' ''-'], ''$nameFilter)), '%_') . '%');
  429.             $qb->andWhere($nameExpr);
  430.         }
  431.         if (null !== $isMasseur) {
  432.             $qb->andWhere('profile.masseur = :is_masseur')
  433.                 ->setParameter('is_masseur'$isMasseur);
  434.         }
  435.         return $qb;
  436.     }
  437.     public function ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterPaged(User $ownerstring $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur null): ORMQueryResult
  438.     {
  439.         $qb $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner$placementTypeFilter$nameFilter$isMasseur);
  440.         //сортируем анкеты по статусу UltraVip->Vip->Standard->Free->Hidden
  441.         $aliases $qb->getAllAliases();
  442.         if (false == in_array('placement'$aliases))
  443.             $qb->leftJoin('profile.adBoardPlacement''placement');
  444.         if (false == in_array('placement_hiding'$aliases))
  445.             $qb->leftJoin('profile.placementHiding''placement_hiding');
  446.         $qb->addSelect('IF(placement_hiding.id IS NULL, 0, 1) as HIDDEN is_hidden');
  447.         $qb->addOrderBy('placement.type''DESC');
  448.         $qb->addOrderBy('placement.placedAt''DESC');
  449.         $qb->addOrderBy('is_hidden''ASC');
  450.         return new ORMQueryResult($qb);
  451.     }
  452.     public function idsOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $ownerstring $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur null): array
  453.     {
  454.         $qb $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner$placementTypeFilter$nameFilter$isMasseur);
  455.         $qb->select('profile.id');
  456.         return $qb->getQuery()->getResult('column_hydrator');
  457.     }
  458.     public function countOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter(User $ownerstring $placementTypeFilter, ?string $nameFilter, ?bool $isMasseur null): int
  459.     {
  460.         $qb $this->queryBuilderOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($owner$placementTypeFilter$nameFilter$isMasseur);
  461.         $qb->select('count(profile.id)')
  462.             ->setMaxResults(1);
  463.         return (int)$qb->getQuery()->getSingleScalarResult();
  464.     }
  465.     /**
  466.      * @deprecated
  467.      */
  468.     public function hydrateProfileRow(array $row): ProfileListingReadModel
  469.     {
  470.         $profile = new ProfileListingReadModel();
  471.         $profile->id $row['id'];
  472.         $profile->city $row['city'];
  473.         $profile->uriIdentity $row['uriIdentity'];
  474.         $profile->name $row['name'];
  475.         $profile->description $row['description'];
  476.         $profile->phoneNumber $row['phoneNumber'];
  477.         $profile->approved $row['approved'];
  478.         $now = new \DateTimeImmutable('now');
  479.         $hasRunningTopPlacement false;
  480.         foreach ($row['topPlacements'] as $topPlacement) {
  481.             if ($topPlacement['placedAt'] <= $now && $now <= $topPlacement['expiresAt'])
  482.                 $hasRunningTopPlacement true;
  483.         }
  484.         $profile->active null !== $row['adBoardPlacement'] || $hasRunningTopPlacement;
  485.         $profile->hidden null != $row['placementHiding'];
  486.         $profile->personParameters = new ProfilePersonParametersReadModel();
  487.         $profile->personParameters->age $row['personParameters.age'];
  488.         $profile->personParameters->height $row['personParameters.height'];
  489.         $profile->personParameters->weight $row['personParameters.weight'];
  490.         $profile->personParameters->breastSize $row['personParameters.breastSize'];
  491.         $profile->personParameters->bodyType $row['personParameters.bodyType'];
  492.         $profile->personParameters->hairColor $row['personParameters.hairColor'];
  493.         $profile->personParameters->privateHaircut $row['personParameters.privateHaircut'];
  494.         $profile->personParameters->nationality $row['personParameters.nationality'];
  495.         $profile->personParameters->hasTattoo $row['personParameters.hasTattoo'];
  496.         $profile->personParameters->hasPiercing $row['personParameters.hasPiercing'];
  497.         $profile->stations $row['stations'];
  498.         $profile->avatar $row['avatar'];
  499.         foreach ($row['photos'] as $photo)
  500.             if ($photo['main'])
  501.                 $profile->mainPhoto $photo;
  502.         $profile->mainPhoto null;
  503.         $profile->photos = [];
  504.         $profile->selfies = [];
  505.         foreach ($row['photos'] as $photo) {
  506.             if ($photo['main'])
  507.                 $profile->mainPhoto $photo;
  508.             if ($photo['type'] == Photo::TYPE_PHOTO)
  509.                 $profile->photos[] = $photo;
  510.             if ($photo['type'] == Photo::TYPE_SELFIE)
  511.                 $profile->selfies[] = $photo;
  512.         }
  513.         $profile->videos $row['videos'];
  514.         $profile->comments $row['comments'];
  515.         $profile->apartmentsPricing = new ProfileApartmentPricingReadModel();
  516.         $profile->apartmentsPricing->oneHourPrice $row['apartmentsPricing.oneHourPrice'];
  517.         $profile->apartmentsPricing->twoHoursPrice $row['apartmentsPricing.twoHoursPrice'];
  518.         $profile->apartmentsPricing->nightPrice $row['apartmentsPricing.nightPrice'];
  519.         $profile->takeOutPricing = new ProfileTakeOutPricingReadModel();
  520.         $profile->takeOutPricing->oneHourPrice $row['takeOutPricing.oneHourPrice'];
  521.         $profile->takeOutPricing->twoHoursPrice $row['takeOutPricing.twoHoursPrice'];
  522.         $profile->takeOutPricing->nightPrice $row['takeOutPricing.nightPrice'];
  523.         return $profile;
  524.     }
  525.     public function deletedByPeriod(\DateTimeInterface $start\DateTimeInterface $end): array
  526.     {
  527.         $qb $this->createQueryBuilder('profile')
  528.             ->join('profile.city''city')
  529.             ->select('profile.uriIdentity _profile')
  530.             ->addSelect('city.uriIdentity _city')
  531.             ->andWhere('profile.deletedAt >= :start')
  532.             ->andWhere('profile.deletedAt <= :end')
  533.             ->setParameter('start'$start)
  534.             ->setParameter('end'$end);
  535.         return $qb->getQuery()->getResult();
  536.     }
  537.     public function listForMapMatchingSpec(Filter|QueryModifier $specificationint $coordinatesRoundPrecision 3): array
  538.     {
  539.         $stmt $this->getEntityManager()->getConnection()->prepare("
  540.             SET SESSION group_concat_max_len = 100000;
  541.         ");
  542.         $stmt->executeStatement();
  543.         /** @var QueryBuilder $qb */
  544.         $qb $this->createQueryBuilder($dqlAlias 'p');
  545.         $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));
  546.         $qb->groupBy('coords');
  547.         $specification->modify($qb$dqlAlias);
  548.         $qb->andWhere($specification->getFilter($qb$dqlAlias));
  549.         return $qb->getQuery()->getResult();
  550.     }
  551.     public function fetchListingByIds(ProfileIdINOrderedByINValues $specification): array
  552.     {
  553.         $ids implode(','$specification->getIds());
  554.         $mediaType $this->features->crop_avatar() ? Photo::TYPE_AVATAR Photo::TYPE_PHOTO;
  555.         $mediaIsMain $this->features->crop_avatar() ? 1;
  556.         $sql "
  557.             SELECT 
  558.                 p.*, JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  559.                     as `name`, 
  560.                 JSON_UNQUOTE(JSON_EXTRACT(p.description, '$.ru')) 
  561.                     as `description`,
  562.                 (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) 
  563.                     as `avatar_path`,
  564.                 (SELECT type FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  565.                     as `adboard_placement_type`,
  566.                 (SELECT position FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  567.                     as `adboard_placement_position`,
  568.                 c.id 
  569.                     as `city_id`, 
  570.                 JSON_UNQUOTE(JSON_EXTRACT(c.name, '$.ru')) 
  571.                     as `city_name`, 
  572.                 c.uri_identity 
  573.                     as `city_uri_identity`,
  574.                 c.country_code 
  575.                     as `city_country_code`,
  576.                 EXISTS(SELECT * FROM profile_top_placements ptp WHERE p.id = ptp.profile_id AND (NOW() BETWEEN ptp.placed_at AND ptp.expires_at))
  577.                     as `has_top_placement`,
  578.                 EXISTS(SELECT * FROM placement_hidings ph WHERE p.id = ph.profile_id AND ph.entity_type = 'profile') 
  579.                     as `has_placement_hiding`,
  580.                 EXISTS(SELECT * FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  581.                     as `has_comments`,
  582.                 EXISTS(SELECT * FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  583.                     as `has_videos`,
  584.                 EXISTS(SELECT * FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  585.                     as `has_selfies`
  586.             FROM profiles `p`
  587.             JOIN cities `c` ON c.id = p.city_id 
  588.             WHERE p.id IN ($ids)
  589.             ORDER BY FIELD(p.id,$ids)";
  590.         $conn $this->getEntityManager()->getConnection();
  591.         $stmt $conn->prepare($sql);
  592.         $result $stmt->executeQuery([]);
  593.         /** @var Statement $stmt */
  594.         $profiles $result->fetchAllAssociative();
  595.         $sql "SELECT 
  596.                     cs.id 
  597.                         as `id`,
  598.                     JSON_UNQUOTE(JSON_EXTRACT(cs.name, '$.ru')) 
  599.                         as `name`, 
  600.                     cs.uri_identity 
  601.                         as `uriIdentity`, 
  602.                     ps.profile_id
  603.                         as `profile_id`,
  604.                     csl.name
  605.                         as `line_name`,
  606.                     csl.color
  607.                         as `line_color`
  608.                 FROM profile_stations ps
  609.                 JOIN city_stations cs ON ps.station_id = cs.id 
  610.                 LEFT JOIN city_subway_station_lines cssl ON cssl.station_id = cs.id
  611.                 LEFT JOIN city_subway_lines csl ON csl.id = cssl.line_id
  612.                 WHERE ps.profile_id IN ($ids)";
  613.         $stmt $conn->prepare($sql);
  614.         $result $stmt->executeQuery([]);
  615.         /** @var Statement $stmt */
  616.         $stations $result->fetchAllAssociative();
  617.         $sql "SELECT 
  618.                     s.id 
  619.                         as `id`,
  620.                     JSON_UNQUOTE(JSON_EXTRACT(s.name, '$.ru')) 
  621.                         as `name`, 
  622.                     s.group 
  623.                         as `group`, 
  624.                     s.uri_identity 
  625.                         as `uriIdentity`,
  626.                     pps.profile_id
  627.                         as `profile_id`,
  628.                     pps.service_condition
  629.                         as `condition`,
  630.                     pps.extra_charge
  631.                         as `extra_charge`,
  632.                     pps.comment
  633.                         as `comment`
  634.                 FROM profile_provided_services pps
  635.                 JOIN services s ON pps.service_id = s.id 
  636.                 WHERE pps.profile_id IN ($ids)
  637.                 ORDER BY s.group ASC, s.sort ASC, s.id ASC";
  638.         $stmt $conn->prepare($sql);
  639.         $result $stmt->executeQuery([]);
  640.         /** @var Statement $stmt */
  641.         $providedServices $result->fetchAllAssociative();
  642.         $result array_map(function ($profile) use ($stations$providedServices): ProfileListingReadModel {
  643.             return $this->hydrateProfileRow2($profile$stations$providedServices);
  644.         }, $profiles);
  645.         return $result;
  646.     }
  647.     public function hydrateProfileRow2(array $row, array $stations, array $services): ProfileListingReadModel
  648.     {
  649.         $profile = new ProfileListingReadModel();
  650.         $profile->id $row['id'];
  651.         $profile->moderationStatus $row['moderation_status'];
  652.         $profile->city = new CityReadModel();
  653.         $profile->city->id $row['city_id'];
  654.         $profile->city->name $row['city_name'];
  655.         $profile->city->uriIdentity $row['city_uri_identity'];
  656.         $profile->city->countryCode $row['city_country_code'];
  657.         $profile->uriIdentity $row['uri_identity'];
  658.         $profile->name $row['name'];
  659.         $profile->description $row['description'];
  660.         $profile->phoneNumber $row['phone_number'];
  661.         $profile->approved = (bool)$row['is_approved'];
  662.         $profile->isUltraVip $row['adboard_placement_type'] == AdBoardPlacement::POSITION_GROUP_ULTRA_VIP;
  663.         $profile->isVip $row['adboard_placement_type'] == AdBoardPlacement::POSITION_GROUP_VIP;
  664.         $profile->isStandard false !== array_search(
  665.                 $row['adboard_placement_type'],
  666.                 [
  667.                     AdBoardPlacement::POSITION_GROUP_STANDARD_APPROVEDAdBoardPlacement::POSITION_GROUP_STANDARD,
  668.                     AdBoardPlacement::POSITION_GROUP_WITHOUT_OWNER_APPROVEDAdBoardPlacement::POSITION_GROUP_WITHOUT_OWNER
  669.                 ]
  670.             );
  671.         $profile->position $row['adboard_placement_position'];
  672.         $profile->active null !== $row['adboard_placement_type'] || $row['has_top_placement'];
  673.         $profile->hidden $row['has_placement_hiding'] == true;
  674.         $profile->personParameters = new ProfilePersonParametersReadModel();
  675.         $profile->personParameters->age $row['person_age'];
  676.         $profile->personParameters->height $row['person_height'];
  677.         $profile->personParameters->weight $row['person_weight'];
  678.         $profile->personParameters->breastSize $row['person_breast_size'];
  679.         $profile->personParameters->bodyType $row['person_body_type'];
  680.         $profile->personParameters->hairColor $row['person_hair_color'];
  681.         $profile->personParameters->privateHaircut $row['person_private_haircut'];
  682.         $profile->personParameters->nationality $row['person_nationality'];
  683.         $profile->personParameters->hasTattoo $row['person_has_tattoo'];
  684.         $profile->personParameters->hasPiercing $row['person_has_piercing'];
  685.         $profile->stations = [];
  686.         foreach ($stations as $station) {
  687.             if ($profile->id !== $station['profile_id'])
  688.                 continue;
  689.             $profileStation $profile->stations[$station['id']] ?? new StationReadModel($station['id'], $station['uriIdentity'], $station['name'], []);
  690.             if (null !== $station['line_name']) {
  691.                 $profileStation->lines[] = new StationLineReadModel($station['line_name'], $station['line_color']);
  692.             }
  693.             $profile->stations[$station['id']] = $profileStation;
  694.         }
  695.         $primaryId = (int)$row['primary_station_id'];
  696.         if (!empty($profile->stations)) {
  697.             uasort($profile->stations, function (StationReadModel $aStationReadModel $b) use ($primaryId) {
  698.                 $aPrimary $a->id === $primaryId;
  699.                 $bPrimary $b->id === $primaryId;
  700.                 if ($aPrimary !== $bPrimary) {
  701.                     return $aPrimary ? -1;
  702.                 }
  703.                 return strnatcasecmp($a->name$b->name);
  704.             });
  705.         }
  706.         $profile->providedServices = [];
  707.         foreach ($services as $service) {
  708.             if ($profile->id !== $service['profile_id'])
  709.                 continue;
  710.             $providedService $profile->providedServices[$service['id']] ?? new ProvidedServiceReadModel(
  711.                 $service['id'], $service['name'], $service['group'], $service['uriIdentity'],
  712.                 $service['condition'], $service['extra_charge'], $service['comment']
  713.             );
  714.             $profile->providedServices[$service['id']] = $providedService;
  715.         }
  716.         $profile->selfies $row['has_selfies'] ? [1] : [];
  717.         $profile->videos $row['has_videos'] ? [1] : [];
  718.         $avatar = [
  719.             'path' => $row['avatar_path'] ?? '',
  720.             'type' => $this->features->crop_avatar() ? Photo::TYPE_AVATAR Photo::TYPE_PHOTO
  721.         ];
  722.         if ($this->features->crop_avatar()) {
  723.             $profile->avatar $avatar;
  724.         } else {
  725.             $profile->mainPhoto $avatar;
  726.             $profile->photos = [];
  727.         }
  728.         $profile->comments $row['has_comments'] ? [1] : [];
  729.         $profile->apartmentsPricing = new ProfileApartmentPricingReadModel();
  730.         $profile->apartmentsPricing->oneHourPrice $row['apartments_one_hour_price'];
  731.         $profile->apartmentsPricing->twoHoursPrice $row['apartments_two_hours_price'];
  732.         $profile->apartmentsPricing->nightPrice $row['apartments_night_price'];
  733.         $profile->takeOutPricing = new ProfileTakeOutPricingReadModel();
  734.         $profile->takeOutPricing->oneHourPrice $row['take_out_one_hour_price'];
  735.         $profile->takeOutPricing->twoHoursPrice $row['take_out_two_hours_price'];
  736.         $profile->takeOutPricing->nightPrice $row['take_out_night_price'];
  737.         $profile->takeOutPricing->locations $row['take_out_locations'] ? array_map('intval'explode(','$row['take_out_locations'])) : [];
  738.         $profile->seo $row['seo'] ? json_decode($row['seo'], true) : null;
  739.         return $profile;
  740.     }
  741.     public function fetchMapProfilesByIds(ProfileIdINOrderedByINValues $specification): array
  742.     {
  743.         $ids implode(','$specification->getIds());
  744.         $mediaType $this->features->crop_avatar() ? Photo::TYPE_AVATAR Photo::TYPE_PHOTO;
  745.         $mediaIsMain $this->features->crop_avatar() ? 1;
  746.         $sql "
  747.             SELECT 
  748.                 p.id, p.uri_identity, p.map_latitude, p.map_longitude, p.phone_number, p.is_masseur, p.is_approved,
  749.                 p.person_age, p.person_breast_size, p.person_height, p.person_weight, pap.type as placement_type, ptp.id as top_placement, p.primary_station_id
  750.                 JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  751.                     as `name`,
  752.                 (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) 
  753.                     as `avatar_path`,
  754.                 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,
  755.                 GROUP_CONCAT(ps.station_id) as `stations`,
  756.                 GROUP_CONCAT(pps.service_id) as `services`,
  757.                 EXISTS(SELECT * FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  758.                     as `has_comments`,
  759.                 EXISTS(SELECT * FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  760.                     as `has_videos`,
  761.                 EXISTS(SELECT * FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  762.                     as `has_selfies`
  763.             FROM profiles `p`
  764.             LEFT JOIN profile_stations ps ON ps.profile_id = p.id
  765.             LEFT JOIN profile_provided_services pps ON pps.profile_id = p.id
  766.             LEFT JOIN profile_adboard_placements pap ON pap.profile_id = p.id
  767.             LEFT JOIN profile_top_placements ptp ON ptp.profile_id = p.id
  768.             WHERE p.id IN ($ids)
  769.             GROUP BY p.id, ptp.id
  770.             "// AND p.map_latitude IS NOT NULL AND p.map_longitude IS NOT NULL; ORDER BY FIELD(p.id,$ids)
  771.         $conn $this->getEntityManager()->getConnection();
  772.         $stmt $conn->prepare($sql);
  773.         $result $stmt->executeQuery([]);
  774.         /** @var Statement $stmt */
  775.         $profiles $result->fetchAllAssociative();
  776.         $result array_map(function ($profile): ProfileMapReadModel {
  777.             return $this->hydrateMapProfileRow($profile);
  778.         }, $profiles);
  779.         return $result;
  780.     }
  781.     public function hydrateMapProfileRow(array $row): ProfileMapReadModel
  782.     {
  783.         $profile = new ProfileMapReadModel();
  784.         $profile->id $row['id'];
  785.         $profile->uriIdentity $row['uri_identity'];
  786.         $profile->name $row['name'];
  787.         $profile->phoneNumber $row['phone_number'];
  788.         $profile->avatar = ['path' => $row['avatar_path'] ?? '''type' => $this->features->crop_avatar() ? Photo::TYPE_AVATAR Photo::TYPE_PHOTO];
  789.         $profile->mapLatitude $row['map_latitude'];
  790.         $profile->mapLongitude $row['map_longitude'];
  791.         $profile->age $row['person_age'];
  792.         $profile->breastSize $row['person_breast_size'];
  793.         $profile->height $row['person_height'];
  794.         $profile->weight $row['person_weight'];
  795.         $profile->isMasseur $row['is_masseur'];
  796.         $profile->isApproved $row['is_approved'];
  797.         $profile->hasComments $row['has_comments'];
  798.         $profile->hasSelfies $row['has_selfies'];
  799.         $profile->hasVideos $row['has_videos'];
  800.         $profile->apartmentOneHourPrice $row['apartments_one_hour_price'];
  801.         $profile->apartmentTwoHoursPrice $row['apartments_two_hours_price'];
  802.         $profile->apartmentNightPrice $row['apartments_night_price'];
  803.         $profile->takeOutOneHourPrice $row['take_out_one_hour_price'];
  804.         $profile->takeOutTwoHoursPrice $row['take_out_two_hours_price'];
  805.         $profile->takeOutNightPrice $row['take_out_night_price'];
  806.         $profile->station $row['primary_station_id'] ?? ($row['stations'] ? explode(','$row['stations'])[0] : null);
  807.         $profile->services $row['services'] ? array_unique(explode(','$row['services'])) : [];
  808.         $profile->isPaid $row['placement_type'] >= AdBoardPlacement::POSITION_GROUP_STANDARD || $row['top_placement'] !== null;
  809. //        $prices = [ $row['apartments_one_hour_price'], $row['apartments_two_hours_price'], $row['apartments_night_price'],
  810. //            $row['take_out_one_hour_price'], $row['take_out_two_hours_price'], $row['take_out_night_price'] ];
  811. //        $prices = array_filter($prices, function($item) {
  812. //            return $item != null;
  813. //        });
  814. //        $profile->price = count($prices) ? min($prices) : null;
  815.         return $profile;
  816.     }
  817.     public function fetchAccountProfileListByIds(ProfileIdINOrderedByINValues $specification): array
  818.     {
  819.         $ids implode(','$specification->getIds());
  820.         $mediaType $this->features->crop_avatar() ? Photo::TYPE_AVATAR Photo::TYPE_PHOTO;
  821.         $mediaIsMain $this->features->crop_avatar() ? 1;
  822.         $sql "
  823.             SELECT 
  824.                 p.*, JSON_UNQUOTE(JSON_EXTRACT(p.name, '$.ru')) 
  825.                     as `name`, 
  826.                 JSON_UNQUOTE(JSON_EXTRACT(p.description, '$.ru')) 
  827.                     as `description`,
  828.                 (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) 
  829.                     as `avatar_path`,
  830.                 (SELECT type FROM profile_adboard_placements pap WHERE p.id = pap.profile_id LIMIT 1) 
  831.                     as `adboard_placement_type`,
  832.                 c.id 
  833.                     as `city_id`, 
  834.                 JSON_UNQUOTE(JSON_EXTRACT(c.name, '$.ru')) 
  835.                     as `city_name`, 
  836.                 c.uri_identity 
  837.                     as `city_uri_identity`,
  838.                 c.country_code 
  839.                     as `city_country_code`,
  840.                 EXISTS(SELECT * FROM profile_top_placements ptp WHERE p.id = ptp.profile_id AND (NOW() BETWEEN ptp.placed_at AND ptp.expires_at))
  841.                     as `has_top_placement`,
  842.                 EXISTS(SELECT * FROM placement_hidings ph WHERE p.id = ph.profile_id AND ph.entity_type = 'profile') 
  843.                     as `has_placement_hiding`,
  844.                 EXISTS(SELECT * FROM profile_comments pc WHERE p.id = pc.profile_id AND pc.deleted_at is NULL) 
  845.                     as `has_comments`,
  846.                 EXISTS(SELECT * FROM profile_media_files pmf_video WHERE p.id = pmf_video.profile_id AND pmf_video.type = 'video') 
  847.                     as `has_videos`,
  848.                 EXISTS(SELECT * FROM profile_media_files pmf_selfie WHERE p.id = pmf_selfie.profile_id AND pmf_selfie.type = 'selfie') 
  849.                     as `has_selfies`
  850.             FROM profiles `p`
  851.             JOIN cities `c` ON c.id = p.city_id 
  852.             WHERE p.id IN ($ids)
  853.             ORDER BY FIELD(p.id,$ids)";
  854.         $conn $this->getEntityManager()->getConnection();
  855.         $stmt $conn->prepare($sql);
  856.         $result $stmt->executeQuery([]);
  857.         /** @var Statement $stmt */
  858.         $profiles $result->fetchAllAssociative();
  859.         $sql "SELECT 
  860.                     JSON_UNQUOTE(JSON_EXTRACT(cs.name, '$.ru')) 
  861.                         as `name`, 
  862.                     cs.uri_identity 
  863.                         as `uriIdentity`, 
  864.                     ps.profile_id
  865.                         as `profile_id` 
  866.                 FROM profile_stations ps
  867.                 JOIN city_stations cs ON ps.station_id = cs.id                 
  868.                 WHERE ps.profile_id IN ($ids)";
  869.         $stmt $conn->prepare($sql);
  870.         $result $stmt->executeQuery([]);
  871.         /** @var Statement $stmt */
  872.         $stations $result->fetchAllAssociative();
  873.         $sql "SELECT 
  874.                     s.id 
  875.                         as `id`,
  876.                     JSON_UNQUOTE(JSON_EXTRACT(s.name, '$.ru')) 
  877.                         as `name`, 
  878.                     s.group 
  879.                         as `group`, 
  880.                     s.uri_identity 
  881.                         as `uriIdentity`,
  882.                     pps.profile_id
  883.                         as `profile_id`,
  884.                     pps.service_condition
  885.                         as `condition`,
  886.                     pps.extra_charge
  887.                         as `extra_charge`,
  888.                     pps.comment
  889.                         as `comment`
  890.                 FROM profile_provided_services pps
  891.                 JOIN services s ON pps.service_id = s.id 
  892.                 WHERE pps.profile_id IN ($ids)
  893.                 ORDER BY s.group ASC, s.sort ASC, s.id ASC";
  894.         $stmt $conn->prepare($sql);
  895.         $result $stmt->executeQuery([]);
  896.         /** @var Statement $stmt */
  897.         $providedServices $result->fetchAllAssociative();
  898.         $result array_map(function ($profile) use ($stations$providedServices): ProfileListingReadModel {
  899.             return $this->hydrateProfileRow2($profile$stations$providedServices);
  900.         }, $profiles);
  901.         return $result;
  902.     }
  903.     public function getCommentedProfilesPaged(User $owner): ORMQueryResult
  904.     {
  905.         $qb $this->createQueryBuilder('profile')
  906.             ->join('profile.comments''comment')
  907.             ->andWhere('profile.owner = :owner')
  908.             ->setParameter('owner'$owner)
  909.             ->orderBy('comment.createdAt''DESC');
  910.         return new ORMQueryResult($qb);
  911.     }
  912.     /**
  913.      * @return ProfilePlacementPriceDetailReadModel[]
  914.      */
  915.     public function fetchOfOwnerPlacedPriceDetails(User $owner): array
  916.     {
  917.         $sql "
  918.             SELECT 
  919.                 p.id, p.is_approved, psp.price_amount
  920.             FROM profiles `p`
  921.             JOIN profile_adboard_placements pap ON pap.profile_id = p.id AND pap.placement_price_id IS NOT NULL
  922.             JOIN paid_service_prices psp ON pap.placement_price_id = psp.id
  923.             WHERE p.user_id = {$owner->getId()}
  924.         ";
  925.         $conn $this->getEntityManager()->getConnection();
  926.         $stmt $conn->prepare($sql);
  927.         $result $stmt->executeQuery([]);
  928.         $profiles $result->fetchAllAssociative();
  929.         return array_map(function (array $row): ProfilePlacementPriceDetailReadModel {
  930.             return new ProfilePlacementPriceDetailReadModel(
  931.                 $row['id'], $row['is_approved'], $row['price_amount'] / 24
  932.             );
  933.         }, $profiles);
  934.     }
  935.     /**
  936.      * @return ProfilePlacementHidingDetailReadModel[]
  937.      */
  938.     public function fetchOfOwnerHiddenDetails(User $owner): array
  939.     {
  940.         $sql "
  941.             SELECT 
  942.                 p.id, p.is_approved
  943.             FROM profiles `p`
  944.             JOIN placement_hidings ph ON ph.profile_id = p.id
  945.             WHERE p.user_id = {$owner->getId()}
  946.         ";
  947.         $conn $this->getEntityManager()->getConnection();
  948.         $stmt $conn->prepare($sql);
  949.         $result $stmt->executeQuery([]);
  950.         $profiles $result->fetchAllAssociative();
  951.         return array_map(function (array $row): ProfilePlacementHidingDetailReadModel {
  952.             return new ProfilePlacementHidingDetailReadModel(
  953.                 $row['id'], $row['is_approved'], true
  954.             );
  955.         }, $profiles);
  956.     }
  957.     protected function modifyListingQueryBuilder(QueryBuilder $qbstring $alias): void
  958.     {
  959.         $qb
  960.             ->addSelect('city')
  961.             ->addSelect('station')
  962.             ->addSelect('photo')
  963.             ->addSelect('video')
  964.             ->addSelect('comment')
  965.             ->addSelect('avatar')
  966.             ->join(sprintf('%s.city'$alias), 'city');
  967.         if (!in_array('station'$qb->getAllAliases()))
  968.             $qb->leftJoin(sprintf('%s.stations'$alias), 'station');
  969.         if (!in_array('photo'$qb->getAllAliases()))
  970.             $qb->leftJoin(sprintf('%s.photos'$alias), 'photo');
  971.         if (!in_array('video'$qb->getAllAliases()))
  972.             $qb->leftJoin(sprintf('%s.videos'$alias), 'video');
  973.         if (!in_array('avatar'$qb->getAllAliases()))
  974.             $qb->leftJoin(sprintf('%s.avatar'$alias), 'avatar');
  975.         if (!in_array('comment'$qb->getAllAliases()))
  976.             $qb->leftJoin(sprintf('%s.comments'$alias), 'comment');
  977.         $this->addFemaleGenderFilterToQb($qb$alias);
  978.         //TODO убрать, если все ок
  979.         //$this->excludeHavingPlacementHiding($qb, $alias);
  980.         if (!in_array('profile_adboard_placement'$qb->getAllAliases())) {
  981.             $qb
  982.                 ->leftJoin(sprintf('%s.adBoardPlacement'$alias), 'profile_adboard_placement');
  983.         }
  984.         $qb->addSelect('profile_adboard_placement');
  985.         if (!in_array('profile_top_placement'$qb->getAllAliases())) {
  986.             $qb
  987.                 ->leftJoin(sprintf('%s.topPlacements'$alias), 'profile_top_placement');
  988.         }
  989.         $qb->addSelect('profile_top_placement');
  990.         //if($this->features->free_profiles()) {
  991.         if (!in_array('placement_hiding'$qb->getAllAliases())) {
  992.             $qb
  993.                 ->leftJoin(sprintf('%s.placementHiding'$alias), 'placement_hiding');
  994.         }
  995.         $qb->addSelect('placement_hiding');
  996.         //}
  997.     }
  998.     protected function addActiveFilterToQb(QueryBuilder $qbstring $dqlAlias)
  999.     {
  1000.         if (!in_array('profile_adboard_placement'$qb->getAllAliases())) {
  1001.             $qb
  1002.                 ->join(sprintf('%s.adBoardPlacement'$dqlAlias), 'profile_adboard_placement');
  1003.         }
  1004.     }
  1005.     private function excludeHavingPlacementHiding(QueryBuilder $qb$alias): void
  1006.     {
  1007.         if ($this->features->free_profiles()) {
  1008. //            if (!in_array('placement_hiding', $qb->getAllAliases())) {
  1009. //                $qb
  1010. //                    ->leftJoin(sprintf('%s.placementHiding', $alias), 'placement_hiding')
  1011. //                    ->andWhere(sprintf('placement_hiding IS NULL'))
  1012. //                ;
  1013. //        }
  1014.             $sub = new QueryBuilder($qb->getEntityManager());
  1015.             $sub->select("exclude_hidden_placement_hiding");
  1016.             $sub->from($qb->getEntityManager()->getClassMetadata(PlacementHiding::class)->name"exclude_hidden_placement_hiding");
  1017.             $sub->andWhere(sprintf('exclude_hidden_placement_hiding.profile = %s'$alias));
  1018.             $qb->andWhere($qb->expr()->not($qb->expr()->exists($sub->getDQL())));
  1019.         }
  1020.     }
  1021. }