diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index aa67aa140c..6af763da79 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -294,6 +294,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberScore::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedDate::class)->setPublic(true); + $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry::class)->setPublic(true); diff --git a/lib/Entities/DynamicSegmentFilterData.php b/lib/Entities/DynamicSegmentFilterData.php index eb45037ce3..7b4306bfb0 100644 --- a/lib/Entities/DynamicSegmentFilterData.php +++ b/lib/Entities/DynamicSegmentFilterData.php @@ -17,6 +17,10 @@ class DynamicSegmentFilterData { public const CONNECT_TYPE_AND = 'and'; public const CONNECT_TYPE_OR = 'or'; + const OPERATOR_ANY = 'any'; + const OPERATOR_ALL = 'all'; + const OPERATOR_NONE = 'none'; + /** * @ORM\Column(type="serialized_array") * @var array|null diff --git a/lib/Segments/DynamicSegments/FilterHandler.php b/lib/Segments/DynamicSegments/FilterHandler.php index 6828eca1bf..50dbe9e2c6 100644 --- a/lib/Segments/DynamicSegments/FilterHandler.php +++ b/lib/Segments/DynamicSegments/FilterHandler.php @@ -47,10 +47,16 @@ class FilterHandler { $this->filterFactory->getFilterForFilterEntity($filter)->apply($subscribersIdsQuery, $filter); } $filterSelects[] = $subscribersIdsQuery->getSQL(); - $queryBuilder->setParameters(array_merge( - $subscribersIdsQuery->getParameters(), - $queryBuilder->getParameters() - )); + $queryBuilder->setParameters( + array_merge( + $subscribersIdsQuery->getParameters(), + $queryBuilder->getParameters() + ), + array_merge( + $subscribersIdsQuery->getParameterTypes(), + $queryBuilder->getParameterTypes() + ) + ); } $this->joinSubqueries($queryBuilder, $segment, $filterSelects); return $queryBuilder; diff --git a/lib/Segments/DynamicSegments/Filters/SubscriberSegment.php b/lib/Segments/DynamicSegments/Filters/SubscriberSegment.php new file mode 100644 index 0000000000..1ef144a392 --- /dev/null +++ b/lib/Segments/DynamicSegments/Filters/SubscriberSegment.php @@ -0,0 +1,62 @@ +entityManager = $entityManager; + } + + public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder { + $filterData = $filter->getFilterData(); + $segments = $filterData->getParam('segments'); + $operator = $filterData->getParam('operator'); + $parameterSuffix = $filter->getId() ?: Security::generateRandomString(); + $statusSubscribedParam = 'subscribed' . $parameterSuffix; + $segmentsParam = 'segments' . $parameterSuffix; + + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); + + $queryBuilder->leftJoin( + $subscribersTable, + $subscriberSegmentTable, + 'subscriber_segments', + "$subscribersTable.id = subscriber_segments.subscriber_id" + . ' AND subscriber_segments.status = :' . $statusSubscribedParam + . ' AND subscriber_segments.segment_id IN (:' . $segmentsParam . ')' + ); + + $queryBuilder->setParameter($statusSubscribedParam, SubscriberEntity::STATUS_SUBSCRIBED); + $queryBuilder->setParameter($segmentsParam, $segments, Connection::PARAM_INT_ARRAY); + + if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) { + $queryBuilder->andWhere('subscriber_segments.id IS NULL'); + } else { + $queryBuilder->andWhere('subscriber_segments.id IS NOT NULL'); + } + + if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) { + $queryBuilder->groupBy('subscriber_id'); + $queryBuilder->having('COUNT(1) = ' . count($segments)); + } + + return $queryBuilder; + } +} diff --git a/tests/integration/Segments/DynamicSegments/Filters/SubscriberSegmentTest.php b/tests/integration/Segments/DynamicSegments/Filters/SubscriberSegmentTest.php new file mode 100644 index 0000000000..ac23bf40e5 --- /dev/null +++ b/tests/integration/Segments/DynamicSegments/Filters/SubscriberSegmentTest.php @@ -0,0 +1,133 @@ +filter = $this->diContainer->get(SubscriberSegment::class); + + $this->cleanUp(); + + $subscriber1 = new SubscriberEntity(); + $subscriber1->setLastSubscribedAt(CarbonImmutable::now()); + $subscriber1->setEmail('a1@example.com'); + $this->entityManager->persist($subscriber1); + + $subscriber2 = new SubscriberEntity(); + $subscriber2->setLastSubscribedAt(CarbonImmutable::now()); + $subscriber2->setEmail('a2@example.com'); + $this->entityManager->persist($subscriber2); + + $subscriber3 = new SubscriberEntity(); + $subscriber3->setLastSubscribedAt(CarbonImmutable::now()); + $subscriber3->setEmail('a3@example.com'); + $this->entityManager->persist($subscriber3); + + $this->segment1 = new SegmentEntity('Segment 1', SegmentEntity::TYPE_DEFAULT, 'Segment 1'); + $this->segment2 = new SegmentEntity('Segment 2', SegmentEntity::TYPE_DEFAULT, 'Segment 2'); + $this->entityManager->persist($this->segment1); + $this->entityManager->persist($this->segment2); + + $this->entityManager->persist(new SubscriberSegmentEntity($this->segment1, $subscriber1, SubscriberEntity::STATUS_SUBSCRIBED)); + + $this->entityManager->persist(new SubscriberSegmentEntity($this->segment2, $subscriber1, SubscriberEntity::STATUS_SUBSCRIBED)); + $this->entityManager->persist(new SubscriberSegmentEntity($this->segment2, $subscriber2, SubscriberEntity::STATUS_SUBSCRIBED)); + $this->entityManager->flush(); + } + + public function testSubscribedAnyOf() { + $segmentFilter = $this->getSegmentFilter(DynamicSegmentFilterData::OPERATOR_ANY, [$this->segment1->getId(), $this->segment2->getId()]); + $statement = $this->filter->apply($this->getQueryBuilder(), $segmentFilter) + ->orderBy('email') + ->execute(); + + $this->assertInstanceOf(Statement::class, $statement); + $result = $statement->fetchAll(); + + expect(count($result))->equals(2); + $subscriber = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber); + expect($subscriber->getEmail())->equals('a1@example.com'); + $subscriber = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber); + expect($subscriber->getEmail())->equals('a2@example.com'); + } + + public function testSubscribedAllOf() { + $segmentFilter = $this->getSegmentFilter(DynamicSegmentFilterData::OPERATOR_ALL, [$this->segment1->getId(), $this->segment2->getId()]); + $statement = $this->filter->apply($this->getQueryBuilder(), $segmentFilter) + ->orderBy('email') + ->execute(); + + $this->assertInstanceOf(Statement::class, $statement); + $result = $statement->fetchAll(); + + expect(count($result))->equals(1); + $subscriber = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber); + expect($subscriber->getEmail())->equals('a1@example.com'); + } + + public function testSubscribedNoneOf() { + $segmentFilter = $this->getSegmentFilter(DynamicSegmentFilterData::OPERATOR_NONE, [$this->segment1->getId()]); + $statement = $this->filter->apply($this->getQueryBuilder(), $segmentFilter) + ->orderBy('email') + ->execute(); + + $this->assertInstanceOf(Statement::class, $statement); + $result = $statement->fetchAll(); + + expect(count($result))->equals(2); + $subscriber = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber); + expect($subscriber->getEmail())->equals('a2@example.com'); + $subscriber = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']); + $this->assertInstanceOf(SubscriberEntity::class, $subscriber); + expect($subscriber->getEmail())->equals('a3@example.com'); + } + + private function getSegmentFilter(string $operator, array $segments): DynamicSegmentFilterEntity { + $segmentFilterData = new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, SubscriberSegment::TYPE, [ + 'operator' => $operator, + 'segments' => $segments, + ]); + $segment = new SegmentEntity('Dynamic Segment', SegmentEntity::TYPE_DYNAMIC, 'description'); + $this->entityManager->persist($segment); + $dynamicSegmentFilter = new DynamicSegmentFilterEntity($segment, $segmentFilterData); + $this->entityManager->persist($dynamicSegmentFilter); + $segment->addDynamicFilter($dynamicSegmentFilter); + return $dynamicSegmentFilter; + } + + private function getQueryBuilder() { + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + return $this->entityManager + ->getConnection() + ->createQueryBuilder() + ->select("DISTINCT $subscribersTable.id") + ->from($subscribersTable); + } + + private function cleanUp() { + $this->truncateEntity(SubscriberEntity::class); + $this->truncateEntity(SegmentEntity::class); + $this->truncateEntity(DynamicSegmentFilterEntity::class); + $this->truncateEntity(SubscriberSegmentEntity::class); + } +}