Implement email actions filter in Doctrine

[MAILPOET-3077]
This commit is contained in:
Rostislav Wolny
2020-09-16 16:55:48 +02:00
committed by Veljko V
parent cf76480ab3
commit 00db901d94
4 changed files with 239 additions and 13 deletions

View File

@ -257,6 +257,7 @@ class ContainerConfigurator implements IContainerConfigurator {
$container->autowire(\MailPoet\Segments\SubscribersFinder::class); $container->autowire(\MailPoet\Segments\SubscribersFinder::class);
$container->autowire(\MailPoet\Segments\SegmentsRepository::class); $container->autowire(\MailPoet\Segments\SegmentsRepository::class);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true); $container->autowire(\MailPoet\Segments\DynamicSegments\Filters\UserRole::class)->setPublic(true);
$container->autowire(\MailPoet\Segments\DynamicSegments\Filters\EmailAction::class)->setPublic(true);
// Services // Services
$container->autowire(\MailPoet\Services\Bridge::class)->setPublic(true); $container->autowire(\MailPoet\Services\Bridge::class)->setPublic(true);
$container->autowire(\MailPoet\Services\AuthorizedEmailsController::class); $container->autowire(\MailPoet\Services\AuthorizedEmailsController::class);

View File

@ -19,6 +19,8 @@ class DynamicSegmentFilterEntity {
use SafeToOneAssociationLoadTrait; use SafeToOneAssociationLoadTrait;
const TYPE_USER_ROLE = 'userRole'; const TYPE_USER_ROLE = 'userRole';
const TYPE_EMAIL = 'email';
const TYPE_WOOCOMMERCE = 'woocommerce';
/** /**
* @ORM\ManyToOne(targetEntity="MailPoet\Entities\SegmentEntity", inversedBy="filters") * @ORM\ManyToOne(targetEntity="MailPoet\Entities\SegmentEntity", inversedBy="filters")

View File

@ -2,7 +2,13 @@
namespace MailPoet\Segments\DynamicSegments\Filters; namespace MailPoet\Segments\DynamicSegments\Filters;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder; use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
use MailPoetVendor\Doctrine\ORM\EntityManager;
class EmailAction implements Filter { class EmailAction implements Filter {
const ACTION_OPENED = 'opened'; const ACTION_OPENED = 'opened';
@ -10,22 +16,67 @@ class EmailAction implements Filter {
const ACTION_CLICKED = 'clicked'; const ACTION_CLICKED = 'clicked';
const ACTION_NOT_CLICKED = 'notClicked'; const ACTION_NOT_CLICKED = 'notClicked';
/** @var string */ /** @var EntityManager */
private $action; private $entityManager;
/** @var int */ public function __construct(EntityManager $entityManager) {
private $newsletterId; $this->entityManager = $entityManager;
/** @var int|null */
private $linkId;
public function __construct(string $action, int $newsletterId, int $linkId = null) {
$this->action = $action;
$this->newsletterId = $newsletterId;
$this->linkId = $linkId;
} }
public function apply(QueryBuilder $queryBuilder): QueryBuilder { public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filterEntity): QueryBuilder {
$action = $filterEntity->getFilterDataParam('action');
$newsletterId = (int)$filterEntity->getFilterDataParam('newsletter_id');
$linkId = $filterEntity->getFilterDataParam('link_id') ? (int)$filterEntity->getFilterDataParam('link_id') : null;
$statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
if (($action === self::ACTION_CLICKED) || ($action === self::ACTION_NOT_CLICKED)) {
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
} else {
$statsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
}
$where = '1';
if (($action === self::ACTION_NOT_CLICKED) || ($action === self::ACTION_NOT_OPENED)) {
$queryBuilder = $queryBuilder->innerJoin(
$subscribersTable,
$statsSentTable,
'statssent',
"$subscribersTable.id = statssent.subscriber_id AND statssent.newsletter_id = :newsletter"
)->leftJoin(
'statssent',
$statsTable,
'stats',
$this->createNotStatsJoinCondition($action, $linkId)
);
$where .= ' AND stats.id IS NULL';
} else {
$queryBuilder = $queryBuilder->innerJoin(
$subscribersTable,
$statsTable,
'stats',
"stats.subscriber_id = $subscribersTable.id AND stats.newsletter_id = :newsletter"
);
}
if ($action === EmailAction::ACTION_CLICKED && $linkId) {
$where .= ' AND stats.link_id = :link';
}
$queryBuilder = $queryBuilder
->andWhere($where)
->setParameter('newsletter', $newsletterId);
if (in_array($action, [EmailAction::ACTION_CLICKED, EmailAction::ACTION_NOT_CLICKED]) && $linkId) {
$queryBuilder = $queryBuilder
->setParameter('link', $linkId);
}
return $queryBuilder; return $queryBuilder;
} }
private function createNotStatsJoinCondition(string $action, int $linkId = null): string {
$clause = "statssent.subscriber_id = stats.subscriber_id AND stats.newsletter_id = :newsletter";
if ($action === EmailAction::ACTION_NOT_CLICKED && $linkId) {
$clause .= ' AND stats.link_id = :link';
}
return $clause;
}
} }

View File

@ -0,0 +1,172 @@
<?php
namespace MailPoet\Segments\DynamicSegments\Filters;
use MailPoet\Entities\DynamicSegmentFilterEntity;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Models\Newsletter;
use MailPoet\Models\StatisticsClicks;
use MailPoet\Models\StatisticsNewsletters;
use MailPoet\Models\StatisticsOpens;
use MailPoet\Models\Subscriber;
class EmailActionTest extends \MailPoetTest {
/** @var EmailAction */
private $emailAction;
public $subscriberOpenedNotClicked;
public $subscriberNotSent;
public $subscriberNotOpened;
public $subscriberOpenedClicked;
public $newsletter;
public function _before() {
$this->emailAction = $this->diContainer->get(EmailAction::class);
$this->newsletter = Newsletter::createOrUpdate([
'subject' => 'newsletter 1',
'status' => 'sent',
'type' => Newsletter::TYPE_NOTIFICATION,
]);
$this->subscriberOpenedClicked = Subscriber::createOrUpdate([
'email' => 'opened_clicked@example.com',
]);
$this->subscriberOpenedNotClicked = Subscriber::createOrUpdate([
'email' => 'opened_not_clicked@example.com',
]);
$this->subscriberNotOpened = Subscriber::createOrUpdate([
'email' => 'not_opened@example.com',
]);
$this->subscriberNotSent = Subscriber::createOrUpdate([
'email' => 'not_sent@example.com',
]);
StatisticsNewsletters::createMultiple([
['newsletter_id' => $this->newsletter->id, 'subscriber_id' => $this->subscriberOpenedClicked->id, 'queue_id' => 1],
['newsletter_id' => $this->newsletter->id, 'subscriber_id' => $this->subscriberOpenedNotClicked->id, 'queue_id' => 1],
['newsletter_id' => $this->newsletter->id, 'subscriber_id' => $this->subscriberNotOpened->id, 'queue_id' => 1],
]);
StatisticsOpens::getOrCreate($this->subscriberOpenedClicked->id, $this->newsletter->id, 1);
StatisticsOpens::getOrCreate($this->subscriberOpenedNotClicked->id, $this->newsletter->id, 1);
StatisticsClicks::createOrUpdateClickCount(1, $this->subscriberOpenedClicked->id, $this->newsletter->id, 1);
}
public function testGetOpened() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_OPENED, $this->newsletter->id);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(2);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
$subscriber2 = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']);
assert($subscriber2 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_clicked@example.com');
expect($subscriber2->getEmail())->equals('opened_not_clicked@example.com');
}
public function testNotOpened() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_NOT_OPENED, $this->newsletter->id);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(1);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('not_opened@example.com');
}
public function testGetClickedWithoutLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_CLICKED, $this->newsletter->id);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(1);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_clicked@example.com');
}
public function testGetClickedWithLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_CLICKED, $this->newsletter->id, 1);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(1);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_clicked@example.com');
}
public function testGetClickedWrongLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_CLICKED, $this->newsletter->id, 2);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(0);
}
public function testGetNotClickedWithLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_NOT_CLICKED, $this->newsletter->id, 1);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(2);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
$subscriber2 = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']);
assert($subscriber2 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_not_clicked@example.com');
expect($subscriber2->getEmail())->equals('not_opened@example.com');
}
public function testGetNotClickedWithWrongLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_NOT_CLICKED, $this->newsletter->id, 2);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(3);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
$subscriber2 = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']);
assert($subscriber2 instanceof SubscriberEntity);
$subscriber3 = $this->entityManager->find(SubscriberEntity::class, $result[2]['id']);
assert($subscriber3 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_clicked@example.com');
expect($subscriber2->getEmail())->equals('opened_not_clicked@example.com');
expect($subscriber3->getEmail())->equals('not_opened@example.com');
}
public function testGetNotClickedWithoutLink() {
$segmentFilter = $this->getSegmentFilter(EmailAction::ACTION_NOT_CLICKED, $this->newsletter->id);
$result = $this->emailAction->apply($this->getQueryBuilder(), $segmentFilter)->execute()->fetchAll();
expect(count($result))->equals(2);
$subscriber1 = $this->entityManager->find(SubscriberEntity::class, $result[0]['id']);
assert($subscriber1 instanceof SubscriberEntity);
$subscriber2 = $this->entityManager->find(SubscriberEntity::class, $result[1]['id']);
assert($subscriber2 instanceof SubscriberEntity);
expect($subscriber1->getEmail())->equals('opened_not_clicked@example.com');
expect($subscriber2->getEmail())->equals('not_opened@example.com');
}
private function getQueryBuilder() {
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
return $this->entityManager
->getConnection()
->createQueryBuilder()
->select("$subscribersTable.id")
->from($subscribersTable);
}
private function getSegmentFilter(string $action, int $newsletterId, int $linkId = null): DynamicSegmentFilterEntity {
return new DynamicSegmentFilterEntity(
new SegmentEntity('segment', SegmentEntity::TYPE_DYNAMIC),
[
'segmentType' => DynamicSegmentFilterEntity::TYPE_EMAIL,
'action' => $action,
'newsletter_id' => $newsletterId,
'link_id' => $linkId,
]
);
}
public function _after() {
$this->cleanData();
}
private function cleanData() {
StatisticsClicks::where('newsletter_id', $this->newsletter->id)->findResultSet()->delete();
StatisticsNewsletters::where('newsletter_id', $this->newsletter->id)->findResultSet()->delete();
StatisticsOpens::where('newsletter_id', $this->newsletter->id)->findResultSet()->delete();
$this->newsletter->delete();
$this->subscriberOpenedClicked->delete();
$this->subscriberOpenedNotClicked->delete();
$this->subscriberNotOpened->delete();
$this->subscriberNotSent->delete();
}
}