| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- <?php declare(strict_types = 1);
- namespace MailPoet\Segments;
- if (!defined('ABSPATH')) exit;
- use MailPoet\Entities\DynamicSegmentFilterData;
- use MailPoet\Entities\DynamicSegmentFilterEntity;
- use MailPoet\Entities\SegmentEntity;
- use MailPoet\Entities\SubscriberEntity;
- use MailPoet\Entities\SubscriberSegmentEntity;
- use MailPoet\InvalidStateException;
- use MailPoet\NotFoundException;
- use MailPoet\Segments\DynamicSegments\FilterHandler;
- use MailPoetVendor\Doctrine\DBAL\Connection;
- use MailPoetVendor\Doctrine\DBAL\Driver\Statement;
- use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
- use MailPoetVendor\Doctrine\ORM\EntityManager;
- use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
- use MailPoetVendor\Doctrine\ORM\QueryBuilder as ORMQueryBuilder;
- class SegmentSubscribersRepository {
- /** @var EntityManager */
- private $entityManager;
- /** @var FilterHandler */
- private $filterHandler;
- public function __construct(
- EntityManager $entityManager,
- FilterHandler $filterHandler
- ) {
- $this->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);
- $result = $this->getSubscribersStatisticsCount($segment);
- return (int)$result[$status ?: 'all'];
- }
- public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null): int {
- $segmentRepository = $this->entityManager->getRepository(SegmentEntity::class);
- $segments = $segmentRepository->findBy(['id' => $segmentIds]);
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- $queryBuilder = $this->createCountQueryBuilder();
- $subQueries = [];
- foreach ($segments as $segment) {
- $segmentQb = $this->createCountQueryBuilder();
- $segmentQb->select("{$subscribersTable}.id AS inner_id");
- if ($segment->isStatic()) {
- $segmentQb = $this->filterSubscribersInStaticSegment($segmentQb, $segment, $status);
- } else {
- $segmentQb = $this->filterSubscribersInDynamicSegment($segmentQb, $segment, $status);
- }
- // inner parameters have to be merged to outer queryBuilder
- $queryBuilder->setParameters(array_merge(
- $segmentQb->getParameters(),
- $queryBuilder->getParameters()
- ));
- $subQueries[] = $segmentQb->getSQL();
- }
- $queryBuilder->innerJoin(
- $subscribersTable,
- sprintf('(%s)', join(' UNION ', $subQueries)),
- 'inner_subscribers',
- "inner_subscribers.inner_id = {$subscribersTable}.id"
- );
- $statement = $this->executeQuery($queryBuilder);
- $result = $statement->fetchColumn();
- return (int)$result;
- }
- /**
- * @param DynamicSegmentFilterData[] $filters
- * @return int
- * @throws InvalidStateException
- */
- public function getDynamicSubscribersCount(array $filters): int {
- $segment = new SegmentEntity('temporary segment', SegmentEntity::TYPE_DYNAMIC, '');
- foreach ($filters as $filter) {
- $segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter));
- }
- $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
- $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null);
- $statement = $this->executeQuery($queryBuilder);
- $result = $statement->fetch();
- return (int)$result['all'];
- }
- private function createCountQueryBuilder(): QueryBuilder {
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- return $this->entityManager
- ->getConnection()
- ->createQueryBuilder()
- ->select("count(DISTINCT $subscribersTable.id)")
- ->from($subscribersTable);
- }
- private function createDynamicStatisticsQueryBuilder(): QueryBuilder {
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- return $this->entityManager
- ->getConnection()
- ->createQueryBuilder()
- ->from($subscribersTable)
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as `all`")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.deleted_at IS NOT NULL
- THEN 1 ELSE 0 END
- ), 0) as trash")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.status = :status_subscribed AND $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_subscribed")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.status = :status_unsubscribed AND $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unsubscribed")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.status = :status_inactive AND $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_inactive")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.status = :status_unconfirmed AND $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unconfirmed")
- ->addSelect("IFNULL(SUM(
- CASE WHEN $subscribersTable.status = :status_bounced AND $subscribersTable.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) 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);
- }
- private function createStaticStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
- $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- return $this->entityManager
- ->getConnection()
- ->createQueryBuilder()
- ->from($subscriberSegmentTable, 'subscriber_segment')
- ->where('subscriber_segment.segment_id = :segment_id')
- ->setParameter('segment_id', $segment->getId())
- ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as `all`')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.deleted_at IS NOT NULL
- THEN 1 ELSE 0 END
- ), 0) as trash')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_subscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN (subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed) AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unsubscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_inactive')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unconfirmed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) 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);
- }
- private function createStaticGlobalStatusStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
- $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- return $this->entityManager
- ->getConnection()
- ->createQueryBuilder()
- ->from($subscriberSegmentTable, 'subscriber_segment')
- ->where('subscriber_segment.segment_id = :segment_id')
- ->setParameter('segment_id', $segment->getId())
- ->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as `all`')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.deleted_at IS NOT NULL
- THEN 1 ELSE 0 END
- ), 0) as trash')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_subscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_subscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_unsubscribed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unsubscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_inactive AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_inactive')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_unconfirmed AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unconfirmed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN subscribers.status = :status_bounced AND subscribers.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) 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);
- }
- public function getSubscribersWithoutSegmentCount(): int {
- $queryBuilder = $this->entityManager->createQueryBuilder();
- $queryBuilder
- ->select('COUNT(DISTINCT s) AS subscribersCount')
- ->from(SubscriberEntity::class, 's');
- $this->addConstraintsForSubscribersWithoutSegment($queryBuilder);
- return (int)$queryBuilder->getQuery()->getSingleScalarResult();
- }
- public function getSubscribersWithoutSegmentStatisticsCount(): array {
- $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
- $queryBuilder = $this->entityManager
- ->getConnection()
- ->createQueryBuilder();
- $queryBuilder
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as `all`')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.deleted_at IS NOT NULL
- THEN 1 ELSE 0 END
- ), 0) as trash')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_subscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unsubscribed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_inactive')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_unconfirmed')
- ->addSelect('IFNULL(SUM(
- CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL
- THEN 1 ELSE 0 END
- ), 0) as :status_bounced')
- ->from($subscribersTable, 's')
- ->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);
- $this->addConstraintsForSubscribersWithoutSegmentToDBAL($queryBuilder);
- $statement = $this->executeQuery($queryBuilder);
- $result = $statement->fetch();
- return $result;
- }
- public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void {
- $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
- $deletedSegmentsQueryBuilder->select('sg.id')
- ->from(SegmentEntity::class, 'sg')
- ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
- $queryBuilder
- ->leftJoin('s.subscriberSegments', 'ssg', Join::WITH,
- (string)$queryBuilder->expr()->andX(
- $queryBuilder->expr()->eq('ssg.subscriber', 's.id'),
- $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
- $queryBuilder->expr()->notIn('ssg.segment', $deletedSegmentsQueryBuilder->getDQL())
- ))
- ->andWhere('s.deletedAt IS NULL')
- ->andWhere('ssg.id IS NULL')
- ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
- }
- public function addConstraintsForSubscribersWithoutSegmentToDBAL(QueryBuilder $queryBuilder): void {
- $deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
- $subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
- $deletedSegmentsQueryBuilder->select('sg.id')
- ->from(SegmentEntity::class, 'sg')
- ->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
- $queryBuilder
- ->leftJoin('s', $subscribersSegmentTable, 'ssg',
- (string)$queryBuilder->expr()->andX(
- $queryBuilder->expr()->eq('ssg.subscriber_id', 's.id'),
- $queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
- $queryBuilder->expr()->notIn('ssg.segment_id', $deletedSegmentsQueryBuilder->getQuery()->getSQL())
- ))
- ->andWhere('ssg.id IS NULL')
- ->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
- }
- 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();
- $parameterName = "segment_{$segment->getId()}"; // When we use this method more times the parameter name has to be unique
- $queryBuilder = $queryBuilder->join(
- $subscribersTable,
- $subscribersSegmentsTable,
- 'subsegment',
- "subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :$parameterName"
- )->andWhere("$subscribersTable.deleted_at IS NULL")
- ->setParameter($parameterName, $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 = [];
- $dynamicFilters = $segment->getDynamicFilters();
- foreach ($dynamicFilters as $dynamicFilter) {
- $filters[] = $dynamicFilter->getFilterData();
- }
- // 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');
- } elseif ($segment instanceof SegmentEntity) {
- $queryBuilder = $this->filterHandler->apply($queryBuilder, $segment);
- }
- $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 getSubscribersGlobalStatusStatisticsCount(SegmentEntity $segment): array {
- if ($segment->isStatic()) {
- $queryBuilder = $this->createStaticGlobalStatusStatisticsQueryBuilder($segment);
- } else {
- $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
- $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
- }
- $statement = $this->executeQuery($queryBuilder);
- return $statement->fetch();
- }
- public function getSubscribersStatisticsCount(SegmentEntity $segment): array {
- if ($segment->isStatic()) {
- $queryBuilder = $this->createStaticStatisticsQueryBuilder($segment);
- } else {
- $queryBuilder = $this->createDynamicStatisticsQueryBuilder();
- $this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
- }
- $statement = $this->executeQuery($queryBuilder);
- return $statement->fetch();
- }
- }
|