entityManager = $entityManager; $this->filterHandler = $filterHandler; } public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array { return $this->loadSubscriberIdsInSegment($segmentId, $candidateIds); } public function getSubscriberIdsInSegment(int $segmentId): array { return $this->loadSubscriberIdsInSegment($segmentId); } public function getSubscribersCount(int $segmentId, string $status = null): int { $segment = $this->getSegment($segmentId); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $queryBuilder = $this->entityManager ->getConnection() ->createQueryBuilder() ->select("count(DISTINCT $subscribersTable.id)") ->from($subscribersTable); if ($segment->isStatic()) { $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, $status); } else { $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, $status); } $statement = $this->executeQuery($queryBuilder); $result = $statement->fetchColumn(); return (int)$result; } public function getSubscribersWithoutSegmentCount(): int { $queryBuilder = $this->entityManager->createQueryBuilder(); return (int)$queryBuilder ->select('COUNT(DISTINCT s) AS subscribersCount') ->from(SubscriberEntity::class, 's') ->leftJoin('s.subscriberSegments', 'ssg') ->leftJoin('ssg.segment', 'sg') ->leftJoin(SubscriberEntity::class, 's2', Join::WITH, (string)$queryBuilder->expr()->eq('s.id', 's2.id')) ->leftJoin('s2.subscriberSegments', 'ssg2', Join::WITH, 'ssg2.status = :statusSubscribed AND sg.id <> ssg2.segment') ->leftJoin('ssg2.segment', 'sg2', Join::WITH, (string)$queryBuilder->expr()->isNull('sg2.deletedAt')) ->andWhere('s.deletedAt IS NULL') ->andWhere('(ssg.status != :statusSubscribed OR ssg.id IS NULL OR sg.deletedAt IS NOT NULL)') ->andWhere('sg2.id IS NULL') ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED) ->getQuery()->getSingleScalarResult(); } /** * This method is fetches list of all segments basic data and count of subscribed subscribers. * @return array */ public function getSimpleSegmentListWithSubscribersCounts(string $type = null): array { $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); $segmentsTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName(); $segmentsDataQuery = $this->entityManager ->getConnection() ->createQueryBuilder() ->select( "segments.id, segments.name, segments.type, COUNT(IF( subsegments.status = :statusSubscribed AND subscribers.deleted_at IS NULL AND subscribers.status= :statusSubscribed , 1, NULL)) as subscribers" )->from($segmentsTable, 'segments') ->leftJoin('segments', $subscribersSegmentsTable, 'subsegments', "subsegments.segment_id = segments.id") ->leftJoin('subsegments', $subscribersTable, 'subscribers', "subscribers.id = subsegments.subscriber_id") ->where('segments.deleted_at IS NULL') ->groupBy('segments.id') ->addGroupBy('segments.name') ->addGroupBy('segments.type') ->orderBy('segments.name') ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED); if ($type) { $segmentsDataQuery ->andWhere('segments.type = :typeParam') ->setParameter('typeParam', $type); } $statement = $this->executeQuery($segmentsDataQuery); $segments = $statement->fetchAll(); // Fetch subscribers counts for dynamic segments and correct data types foreach ($segments as $key => $segment) { if ($segment['type'] === SegmentEntity::TYPE_DYNAMIC) { $segments[$key]['subscribers'] = $this->getSubscribersCount((int)$segment['id'], SubscriberEntity::STATUS_SUBSCRIBED); } else { $segments[$key]['subscribers'] = (int)$segment['subscribers']; } } return $segments; } private function loadSubscriberIdsInSegment(int $segmentId, array $candidateIds = null): array { $segment = $this->getSegment($segmentId); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $queryBuilder = $this->entityManager ->getConnection() ->createQueryBuilder() ->select("DISTINCT $subscribersTable.id") ->from($subscribersTable); if ($segment->isStatic()) { $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED); } else { $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED); } if ($candidateIds) { $queryBuilder->andWhere("$subscribersTable.id IN (:candidateIds)") ->setParameter('candidateIds', $candidateIds, Connection::PARAM_STR_ARRAY); } $statement = $this->executeQuery($queryBuilder); $result = $statement->fetchAll(); return array_column($result, 'id'); } private function filterSubscribersInStaticSegment( QueryBuilder $queryBuilder, SegmentEntity $segment, string $status = null ): QueryBuilder { $subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $queryBuilder = $queryBuilder->join( $subscribersTable, $subscribersSegmentsTable, 'subsegment', "subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :segment" )->andWhere("$subscribersTable.deleted_at IS NULL") ->setParameter('segment', $segment->getId()); if ($status) { $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status") ->andWhere("subsegment.status = :status") ->setParameter('status', $status); } return $queryBuilder; } private function filterSubscribersInDynamicSegment( QueryBuilder $queryBuilder, SegmentEntity $segment, string $status = null ): QueryBuilder { $filters = $segment->getDynamicFilters(); // We don't allow dynamic segment without filers since it would return all subscribers // For BC compatibility fetching an empty result if (count($filters) === 0) { return $queryBuilder->andWhere('0 = 1'); } foreach ($filters as $filter) { $queryBuilder = $this->filterHandler->apply($queryBuilder, $filter); } $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $queryBuilder = $queryBuilder->andWhere("$subscribersTable.deleted_at IS NULL"); if ($status) { $queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status") ->setParameter('status', $status); } return $queryBuilder; } private function getSegment(int $id): SegmentEntity { $segment = $this->entityManager->find(SegmentEntity::class, $id); if (!$segment instanceof SegmentEntity) { throw new NotFoundException('Segment not found'); } return $segment; } private function executeQuery(QueryBuilder $queryBuilder): Statement { $statement = $queryBuilder->execute(); // Execute for select always returns statement but PHP Stan doesn't know that :( if (!$statement instanceof Statement) { throw new InvalidStateException('Invalid query.'); } return $statement; } public function getSubscribersStatisticsCount(SegmentEntity $segment) { $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $queryBuilder = $this->entityManager ->getConnection() ->createQueryBuilder() ->from($subscriberSegmentTable, 'subscriber_segment') ->where('subscriber_segment.segment_id = :segment_id') ->setParameter('segment_id', $segment->getId()) ->andWhere('subscribers.deleted_at is null') ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id') ->addSelect('SUM( CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed THEN 1 ELSE 0 END ) as :status_subscribed') ->addSelect('SUM( CASE WHEN subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed THEN 1 ELSE 0 END ) as :status_unsubscribed') ->addSelect('SUM( CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed THEN 1 ELSE 0 END ) as :status_inactive') ->addSelect('SUM( CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed THEN 1 ELSE 0 END ) as :status_unconfirmed') ->addSelect('SUM( CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed THEN 1 ELSE 0 END ) as :status_bounced') ->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED) ->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED) ->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE) ->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED) ->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED); $statement = $this->executeQuery($queryBuilder); $result = $statement->fetch(); return $result; } }