From 5b45bdac1e0ba38e99d20aad7f6883c8863ba892 Mon Sep 17 00:00:00 2001 From: Pavel Dohnal Date: Tue, 16 Mar 2021 12:57:30 +0100 Subject: [PATCH] Enable joining segments using and/or [MAILPOET-3212] --- .../DynamicSegments/FilterHandler.php | 24 ++++- .../DynamicSegments/Filters/EmailAction.php | 1 - .../DynamicSegments/Filters/UserRole.php | 1 - .../Filters/WooCommerceCategory.php | 1 - .../Filters/WooCommerceProduct.php | 1 - .../DynamicSegments/FilterHandlerTest.php | 98 ++++++++++++++++++- 6 files changed, 120 insertions(+), 6 deletions(-) diff --git a/lib/Segments/DynamicSegments/FilterHandler.php b/lib/Segments/DynamicSegments/FilterHandler.php index 0a0c0a8c85..0e4021d286 100644 --- a/lib/Segments/DynamicSegments/FilterHandler.php +++ b/lib/Segments/DynamicSegments/FilterHandler.php @@ -11,6 +11,7 @@ use MailPoet\Segments\DynamicSegments\Filters\EmailAction; use MailPoet\Segments\DynamicSegments\Filters\UserRole; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory; use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct; +use MailPoet\Util\Security; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; use MailPoetVendor\Doctrine\ORM\EntityManager; @@ -61,7 +62,28 @@ class FilterHandler { $queryBuilder->getParameters() )); } - $queryBuilder->innerJoin($subscribersTable, sprintf('(%s)', join(' UNION ', $filterSelects)), 'filtered_subscribers', 'filtered_subscribers.inner_subscriber_id = id'); + $this->joinSubqueries($queryBuilder, $segment, $filterSelects); + return $queryBuilder; + } + + private function joinSubqueries(QueryBuilder $queryBuilder, SegmentEntity $segment, array $subQueries): QueryBuilder { + $filter = $segment->getDynamicFilters()->first(); + if (!$filter) return $queryBuilder; + $filterData = $filter->getFilterData(); + $data = $filterData->getData(); + $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); + + if (!isset($data['connect']) || $data['connect'] === 'or') { + // the final query: SELECT * FROM subscribers INNER JOIN (filter_select1 UNION filter_select2) filtered_subscribers ON filtered_subscribers.inner_subscriber_id = id + $queryBuilder->innerJoin($subscribersTable, sprintf('(%s)', join(' UNION ', $subQueries)), 'filtered_subscribers', 'filtered_subscribers.inner_subscriber_id = id'); + return $queryBuilder; + } + + foreach ($subQueries as $subQuery) { + // we need a unique name for each subquery so that we can join them together in the sql query - just make sure the identifier starts with a letter, not a number + $subqueryName = 'a' . Security::generateRandomString(5); + $queryBuilder->innerJoin($subscribersTable, "($subQuery)", $subqueryName, "$subqueryName.inner_subscriber_id = id"); + } return $queryBuilder; } diff --git a/lib/Segments/DynamicSegments/Filters/EmailAction.php b/lib/Segments/DynamicSegments/Filters/EmailAction.php index c528719f52..514d543cd2 100644 --- a/lib/Segments/DynamicSegments/Filters/EmailAction.php +++ b/lib/Segments/DynamicSegments/Filters/EmailAction.php @@ -2,7 +2,6 @@ namespace MailPoet\Segments\DynamicSegments\Filters; -use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\StatisticsClickEntity; use MailPoet\Entities\StatisticsNewsletterEntity; diff --git a/lib/Segments/DynamicSegments/Filters/UserRole.php b/lib/Segments/DynamicSegments/Filters/UserRole.php index 47d6676e4b..ac55106adc 100644 --- a/lib/Segments/DynamicSegments/Filters/UserRole.php +++ b/lib/Segments/DynamicSegments/Filters/UserRole.php @@ -2,7 +2,6 @@ namespace MailPoet\Segments\DynamicSegments\Filters; -use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException; diff --git a/lib/Segments/DynamicSegments/Filters/WooCommerceCategory.php b/lib/Segments/DynamicSegments/Filters/WooCommerceCategory.php index 8d1c335f4f..e1851f19e8 100644 --- a/lib/Segments/DynamicSegments/Filters/WooCommerceCategory.php +++ b/lib/Segments/DynamicSegments/Filters/WooCommerceCategory.php @@ -2,7 +2,6 @@ namespace MailPoet\Segments\DynamicSegments\Filters; -use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\WP\Functions as WPFunctions; diff --git a/lib/Segments/DynamicSegments/Filters/WooCommerceProduct.php b/lib/Segments/DynamicSegments/Filters/WooCommerceProduct.php index fc43571f95..8d9bd5c8f1 100644 --- a/lib/Segments/DynamicSegments/Filters/WooCommerceProduct.php +++ b/lib/Segments/DynamicSegments/Filters/WooCommerceProduct.php @@ -2,7 +2,6 @@ namespace MailPoet\Segments\DynamicSegments\Filters; -use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; use MailPoet\Entities\SubscriberEntity; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; diff --git a/tests/integration/Segments/DynamicSegments/FilterHandlerTest.php b/tests/integration/Segments/DynamicSegments/FilterHandlerTest.php index d83a0efe13..3e75a9ef12 100644 --- a/tests/integration/Segments/DynamicSegments/FilterHandlerTest.php +++ b/tests/integration/Segments/DynamicSegments/FilterHandlerTest.php @@ -4,14 +4,26 @@ namespace MailPoet\Segments\DynamicSegments; use MailPoet\Entities\DynamicSegmentFilterData; use MailPoet\Entities\DynamicSegmentFilterEntity; +use MailPoet\Entities\NewsletterEntity; +use MailPoet\Entities\ScheduledTaskEntity; use MailPoet\Entities\SegmentEntity; +use MailPoet\Entities\SendingQueueEntity; +use MailPoet\Entities\StatisticsOpenEntity; use MailPoet\Entities\SubscriberEntity; +use MailPoet\Segments\DynamicSegments\Filters\EmailAction; +use MailPoet\Subscribers\SubscribersRepository; class FilterHandlerTest extends \MailPoetTest { /** @var FilterHandler */ private $filterHandler; + /** @var SubscriberEntity */ + private $subscriber1; + + /** @var SubscriberEntity */ + private $subscriber2; + public function _before() { $this->cleanWpUsers(); $this->filterHandler = $this->diContainer->get(FilterHandler::class); @@ -30,6 +42,12 @@ class FilterHandlerTest extends \MailPoetTest { 'email' => 'user-role-test3@example.com', 'wp_user_id' => $id, ]); + + // fetch entities + /** @var SubscribersRepository $subscribersRepository */ + $subscribersRepository = $this->diContainer->get(SubscribersRepository::class); + $this->subscriber1 = $subscribersRepository->findOneBy(['email' => 'user-role-test1@example.com']); + $this->subscriber2 = $subscribersRepository->findOneBy(['email' => 'user-role-test2@example.com']); } public function testItAppliesFilter() { @@ -43,7 +61,7 @@ class FilterHandlerTest extends \MailPoetTest { expect($subscriber2->getEmail())->equals('user-role-test3@example.com'); } - public function testItAppliesTwoFilters() { + public function testItAppliesTwoFiltersWithoutSpecifyingConnection() { $segment = $this->getSegment('editor'); $filter = new DynamicSegmentFilterData([ 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, @@ -52,11 +70,84 @@ class FilterHandlerTest extends \MailPoetTest { $dynamicSegmentFilter = new DynamicSegmentFilterEntity($segment, $filter); $this->entityManager->persist($dynamicSegmentFilter); $segment->addDynamicFilter($dynamicSegmentFilter); + $this->entityManager->flush(); $queryBuilder = $this->filterHandler->apply($this->getQueryBuilder(), $segment); $result = $queryBuilder->execute()->fetchAll(); expect($result)->count(3); } + public function testItAppliesTwoFiltersWithOr() { + $segment = new SegmentEntity('Dynamic Segment', SegmentEntity::TYPE_DYNAMIC, 'description'); + $this->entityManager->persist($segment); + $filterData = new DynamicSegmentFilterData([ + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'administrator', + 'connect' => 'or', + ]); + $dynamicSegmentFilter = new DynamicSegmentFilterEntity($segment, $filterData); + $this->entityManager->persist($dynamicSegmentFilter); + $segment->addDynamicFilter($dynamicSegmentFilter); + $filterData = new DynamicSegmentFilterData([ + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + 'connect' => 'or', + ]); + $dynamicSegmentFilter = new DynamicSegmentFilterEntity($segment, $filterData); + $this->entityManager->persist($dynamicSegmentFilter); + $segment->addDynamicFilter($dynamicSegmentFilter); + $this->entityManager->flush(); + $queryBuilder = $this->filterHandler->apply($this->getQueryBuilder(), $segment); + $result = $queryBuilder->execute()->fetchAll(); + expect($result)->count(3); + } + + public function testItAppliesTwoFiltersWithAnd() { + $segment = new SegmentEntity('Dynamic Segment', SegmentEntity::TYPE_DYNAMIC, 'description'); + $this->entityManager->persist($segment); + // filter user is an editor + $editorData = new DynamicSegmentFilterData([ + 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, + 'wordpressRole' => 'editor', + 'connect' => 'and', + ]); + $filterEditor = new DynamicSegmentFilterEntity($segment, $editorData); + $this->entityManager->persist($filterEditor); + $segment->addDynamicFilter($filterEditor); + // filter user opened an email + $newsletter = new NewsletterEntity(); + $task = new ScheduledTaskEntity(); + $this->entityManager->persist($task); + $queue = new SendingQueueEntity(); + $queue->setNewsletter($newsletter); + $queue->setTask($task); + $this->entityManager->persist($queue); + $newsletter->getQueues()->add($queue); + $newsletter->setSubject('newsletter 1'); + $newsletter->setStatus('sent'); + $newsletter->setType(NewsletterEntity::TYPE_STANDARD); + $this->entityManager->persist($newsletter); + $open = new StatisticsOpenEntity($newsletter, $queue, $this->subscriber1); + $this->entityManager->persist($open); + $open = new StatisticsOpenEntity($newsletter, $queue, $this->subscriber2); + $this->entityManager->persist($open); + $this->entityManager->flush(); + + $openedData = new DynamicSegmentFilterData([ + 'segmentType' => DynamicSegmentFilterData::TYPE_EMAIL, + 'action' => EmailAction::ACTION_OPENED, + 'newsletter_id' => $newsletter->getId(), + 'connect' => 'and', + ]); + $filterOpened = new DynamicSegmentFilterEntity($segment, $openedData); + $this->entityManager->persist($filterOpened); + $segment->addDynamicFilter($filterOpened); + $this->entityManager->flush(); + + $queryBuilder = $this->filterHandler->apply($this->getQueryBuilder(), $segment); + $result = $queryBuilder->execute()->fetchAll(); + expect($result)->count(1); + } + private function getSegment(string $role): SegmentEntity { $filter = new DynamicSegmentFilterData([ 'segmentType' => DynamicSegmentFilterData::TYPE_USER_ROLE, @@ -84,6 +175,11 @@ class FilterHandlerTest extends \MailPoetTest { $this->cleanWpUsers(); $this->truncateEntity(SubscriberEntity::class); $this->truncateEntity(SegmentEntity::class); + $this->truncateEntity(DynamicSegmentFilterEntity::class); + $this->truncateEntity(NewsletterEntity::class); + $this->truncateEntity(StatisticsOpenEntity::class); + $this->truncateEntity(SendingQueueEntity::class); + $this->truncateEntity(ScheduledTaskEntity::class); } private function cleanWpUsers() {