diff --git a/lib/Segments/SegmentSubscribersRepository.php b/lib/Segments/SegmentSubscribersRepository.php index 5333bab4a5..30828b25f1 100644 --- a/lib/Segments/SegmentSubscribersRepository.php +++ b/lib/Segments/SegmentSubscribersRepository.php @@ -1,7 +1,8 @@ -entityManager = $entityManager; $this->filterHandler = $filterHandler; + $this->transientCache = $transientCache; } public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array { @@ -42,16 +48,8 @@ class SegmentSubscribersRepository { public function getSubscribersCount(int $segmentId, string $status = null): int { $segment = $this->getSegment($segmentId); - $queryBuilder = $this->createCountQueryBuilder(); - - 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; + $result = $this->getSubscribersStatisticsCount($segment); + return (int)$result[$status ?: 'all']; } public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null): int { @@ -101,11 +99,11 @@ class SegmentSubscribersRepository { foreach ($filters as $filter) { $segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter)); } - $queryBuilder = $this->createCountQueryBuilder(); + $queryBuilder = $this->createDynamicStatisticsQueryBuilder(); $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null); $statement = $this->executeQuery($queryBuilder); - $result = $statement->fetchColumn(); - return (int)$result; + $result = $statement->fetch(); + return (int)$result['all']; } private function createCountQueryBuilder(): QueryBuilder { @@ -117,18 +115,140 @@ class SegmentSubscribersRepository { ->from($subscribersTable); } - public function getSubscribersWithoutSegmentCount(): int { - $queryBuilder = $this->getSubscribersWithoutSegmentCountQuery(); - return (int)$queryBuilder->getQuery()->getSingleScalarResult(); + private function createDynamicStatisticsQueryBuilder(): QueryBuilder { + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + return $this->entityManager + ->getConnection() + ->createQueryBuilder() + ->from($subscribersTable) + ->andWhere("$subscribersTable.deleted_at IS NULL") + ->addSelect("COUNT(DISTINCT $subscribersTable.id) as `all`") + ->addSelect("SUM( + CASE WHEN $subscribersTable.status = :status_subscribed + THEN 1 ELSE 0 END + ) as :status_subscribed") + ->addSelect("SUM( + CASE WHEN $subscribersTable.status = :status_unsubscribed + THEN 1 ELSE 0 END + ) as :status_unsubscribed") + ->addSelect("SUM( + CASE WHEN $subscribersTable.status = :status_inactive + THEN 1 ELSE 0 END + ) as :status_inactive") + ->addSelect("SUM( + CASE WHEN $subscribersTable.status = :status_unconfirmed + THEN 1 ELSE 0 END + ) as :status_unconfirmed") + ->addSelect("SUM( + CASE WHEN $subscribersTable.status = :status_bounced + 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); } - public function getSubscribersWithoutSegmentCountQuery(): ORMQueryBuilder { + private function createStaticStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder { + $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("COUNT(DISTINCT subscribers.id) as `all`") + ->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); + return $queryBuilder; + } + + public function getSubscribersWithoutSegmentCount(): int { $queryBuilder = $this->entityManager->createQueryBuilder(); $queryBuilder ->select('COUNT(DISTINCT s) AS subscribersCount') ->from(SubscriberEntity::class, 's'); $this->addConstraintsForSubscribersWithoutSegment($queryBuilder); - return $queryBuilder; + return (int)$queryBuilder->getQuery()->getSingleScalarResult(); + } + + public function getSubscribersWithoutSegmentStatisticsCount(): array { + $id = 0; + $result = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $id); + if (!$result) { + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $queryBuilder = $this->entityManager + ->getConnection() + ->createQueryBuilder(); + $queryBuilder + ->addSelect("SUM( + CASE WHEN s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) as `all`") + ->addSelect("SUM( + CASE WHEN s.deleted_at IS NOT NULL + THEN 1 ELSE 0 END + ) as trash") + ->addSelect("SUM( + CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) as :status_subscribed") + ->addSelect("SUM( + CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) as :status_unsubscribed") + ->addSelect("SUM( + CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) as :status_inactive") + ->addSelect("SUM( + CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) as :status_unconfirmed") + ->addSelect("SUM( + CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL + THEN 1 ELSE 0 END + ) 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(); + $this->transientCache->setItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $result, $id); + } + return $result; } public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void { @@ -149,6 +269,24 @@ class SegmentSubscribersRepository { ->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(); @@ -242,45 +380,23 @@ class SegmentSubscribersRepository { } 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') + $result = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, (int)$segment->getId()); + if (!$result) { + if ($segment->isStatic()) { + $queryBuilder = $this->createStaticStatisticsQueryBuilder($segment); + } else { + $queryBuilder = $this->createDynamicStatisticsQueryBuilder(); + $this->filterSubscribersInDynamicSegment($queryBuilder, $segment); + } - ->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(); + $statement = $this->executeQuery($queryBuilder); + $result = $statement->fetch(); + $this->transientCache->setItem( + TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, + $result, + (int)$segment->getId() + ); + } return $result; } } diff --git a/lib/Segments/SegmentsSimpleListRepository.php b/lib/Segments/SegmentsSimpleListRepository.php index bb80da892a..329077a04d 100644 --- a/lib/Segments/SegmentsSimpleListRepository.php +++ b/lib/Segments/SegmentsSimpleListRepository.php @@ -51,11 +51,12 @@ class SegmentsSimpleListRepository { * @return array */ public function addVirtualSubscribersWithoutListSegment(array $segments): array { + $withoutSegmentStats = $this->segmentsSubscriberRepository->getSubscribersWithoutSegmentStatisticsCount(); $segments[] = [ 'id' => '0', 'type' => SegmentEntity::TYPE_WITHOUT_LIST, 'name' => __('Subscribers without a list', 'mailpoet'), - 'subscribers' => $this->segmentsSubscriberRepository->getSubscribersWithoutSegmentCount(), + 'subscribers' => $withoutSegmentStats['all'], ]; return $segments; } diff --git a/lib/Subscribers/SubscriberListingRepository.php b/lib/Subscribers/SubscriberListingRepository.php index 005e798349..11873c1c48 100644 --- a/lib/Subscribers/SubscriberListingRepository.php +++ b/lib/Subscribers/SubscriberListingRepository.php @@ -223,29 +223,24 @@ class SubscriberListingRepository extends ListingRepository { public function getFilters(ListingDefinition $definition): array { $group = $definition->getGroup(); - $queryBuilder = clone $this->queryBuilder; - $this->applyFromClause($queryBuilder); - $subscribersWithoutSegmentQuery = $this->segmentSubscribersRepository->getSubscribersWithoutSegmentCountQuery(); + $subscribersWithoutSegmentStats = $this->segmentSubscribersRepository->getSubscribersWithoutSegmentStatisticsCount(); + $key = $group ?: 'all'; + $subscribersWithoutSegmentCount = $subscribersWithoutSegmentStats[$key]; - if ($group) { - $this->applyGroup($queryBuilder, $group); - $this->applyGroup($subscribersWithoutSegmentQuery, $group); - } - - $subscribersWithoutSegment = $subscribersWithoutSegmentQuery->getQuery()->getSingleScalarResult(); $subscribersWithoutSegmentLabel = sprintf( WPFunctions::get()->__('Subscribers without a list (~%s)', 'mailpoet'), - number_format((float)$subscribersWithoutSegment) + number_format((float)$subscribersWithoutSegmentCount) ); + $queryBuilder = clone $this->queryBuilder; $queryBuilder - ->select('sg.id, sg.name, COUNT(s) AS subscribersCount') - ->leftJoin('s.subscriberSegments', 'ssg') - ->join('ssg.segment', 'sg') - ->groupBy('sg.id') - ->andWhere('sg.deletedAt IS NULL') - ->andWhere('s.deletedAt IS NULL') - ->having('subscribersCount > 0'); + ->select('s') + ->from(SegmentEntity::class, 's'); + if ($group === 'trash') { + $queryBuilder->andWhere('s.deletedAt IS NOT NULL'); + } else { + $queryBuilder->andWhere('s.deletedAt IS NULL'); + } // format segment list $allSubscribersList = [ @@ -259,32 +254,19 @@ class SubscriberListingRepository extends ListingRepository { ]; $segmentList = []; - foreach ($queryBuilder->getQuery()->getResult() as $item) { - $segmentList[] = [ - 'label' => sprintf('%s (~%s)', $item['name'], number_format((float)$item['subscribersCount'])), - 'value' => $item['id'], - ]; - } - - $queryBuilder = clone $this->queryBuilder; - // Load dynamic segments with some subscribers - $queryBuilder - ->select('s') - ->from(SegmentEntity::class, 's') - ->andWhere('s.type = :dynamicType') - ->andWhere('s.deletedAt IS NULL') - ->setParameter('dynamicType', SegmentEntity::TYPE_DYNAMIC); - foreach ($queryBuilder->getQuery()->getResult() as $segment) { - $count = $this->segmentSubscribersRepository->getSubscribersCount($segment->getId()); - if (!$count) { + $key = $group ?: 'all'; + $count = $this->segmentSubscribersRepository->getSubscribersStatisticsCount($segment); + if (!$count[$key]) { continue; } + $segmentList[] = [ - 'label' => sprintf('%s (~%s)', $segment->getName(), number_format((float)$count)), + 'label' => sprintf('%s (~%s)', $segment->getName(), number_format((float)$count[$key])), 'value' => $segment->getId(), ]; } + usort($segmentList, function($a, $b) { return strcasecmp($a['label'], $b['label']); });