diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index dc587e5b89..da8ab9dd4c 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -256,8 +256,10 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\WooCommerce::class)->setPublic(true); $container->autowire(\MailPoet\Segments\SubscribersFinder::class); $container->autowire(\MailPoet\Segments\SegmentsRepository::class); - $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\SegmentSubscribersRepository::class); + $container->autowire(\MailPoet\Segments\DynamicSegments\FilterHandler::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailAction::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory::class)->setPublic(true); // Services diff --git a/lib/Entities/SegmentEntity.php b/lib/Entities/SegmentEntity.php index d9536d487a..f6593e0161 100644 --- a/lib/Entities/SegmentEntity.php +++ b/lib/Entities/SegmentEntity.php @@ -105,4 +105,8 @@ class SegmentEntity { public function getDynamicFilters() { return $this->dynamicFilters; } + + public function isDynamic(): bool { + return $this->getType() === self::TYPE_DYNAMIC; + } } diff --git a/lib/Segments/DynamicSegments/FilterHandler.php b/lib/Segments/DynamicSegments/FilterHandler.php new file mode 100644 index 0000000000..0047026bab --- /dev/null +++ b/lib/Segments/DynamicSegments/FilterHandler.php @@ -0,0 +1,54 @@ +emailAction = $emailAction; + $this->userRole = $userRole; + $this->wooCommerceProduct = $wooCommerceProduct; + $this->wooCommerceCategory = $wooCommerceCategory; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filterEntity): QueryBuilder { + switch ($filterEntity->getSegmentType()) { + case DynamicSegmentFilterEntity::TYPE_USER_ROLE: + return $this->userRole->apply($queryBuilder, $filterEntity); + case DynamicSegmentFilterEntity::TYPE_EMAIL: + return $this->emailAction->apply($queryBuilder, $filterEntity); + case DynamicSegmentFilterEntity::TYPE_WOOCOMMERCE: + $action = $filterEntity->getFilterDataParam('action'); + if ($action === WooCommerceProduct::ACTION_PRODUCT) { + return $this->wooCommerceProduct->apply($queryBuilder, $filterEntity); + } + return $this->wooCommerceCategory->apply($queryBuilder, $filterEntity); + default: + throw new InvalidSegmentTypeException('Invalid type', InvalidSegmentTypeException::INVALID_TYPE); + } + } +} diff --git a/lib/Segments/SegmentSubscribersRepository.php b/lib/Segments/SegmentSubscribersRepository.php new file mode 100644 index 0000000000..29b5abd947 --- /dev/null +++ b/lib/Segments/SegmentSubscribersRepository.php @@ -0,0 +1,111 @@ +entityManager = $entityManager; + $this->filterHandler = $filterHandler; + } + + public function getSubscriberIdsInSegment(int $segmentId): 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->isDynamic()) { + $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment); + } else { + $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment); + } + $statement = $this->executeQuery($queryBuilder); + $result = $statement->fetchAll(); + return array_column($result, 'id'); + } + + public function getSubscribersCount(int $segmentId): int { + $segment = $this->getSegment($segmentId); + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $queryBuilder = $this->entityManager + ->getConnection() + ->createQueryBuilder() + ->select("DISTINCT count($subscribersTable.id)") + ->from($subscribersTable); + + if ($segment->isDynamic()) { + $queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment); + } else { + $queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment); + } + $statement = $this->executeQuery($queryBuilder); + $result = $statement->fetchColumn(); + return (int)$result; + } + + private function filterSubscribersInStaticSegment(QueryBuilder $queryBuilder, SegmentEntity $segment): QueryBuilder { + $subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + return $queryBuilder->join( + $subscribersTable, + $subscribersSegmentsTable, + 'subsegment', + "subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :segment" + )->andWhere("$subscribersTable.deleted_at IS NULL") + ->andWhere("$subscribersTable.status = :status") + ->setParameter('segment', $segment->getId()) + ->setParameter('status', SubscriberEntity::STATUS_SUBSCRIBED); + } + + private function filterSubscribersInDynamicSegment(QueryBuilder $queryBuilder, SegmentEntity $segment): QueryBuilder { + $filters = $segment->getDynamicFilters(); + // We don't allow dynamic segment without filers since it would return all subscribers + if (count($filters) === 0) { + throw new InvalidStateException('Missing filters for dynamic segment.'); + } + foreach ($filters as $filter) { + $queryBuilder = $this->filterHandler->apply($queryBuilder, $filter); + } + 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; + } +}