init
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Statistics;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class NewsletterStatistics {
|
||||
|
||||
/** @var int */
|
||||
private $clickCount;
|
||||
|
||||
/** @var int */
|
||||
private $openCount;
|
||||
|
||||
/** @var int */
|
||||
private $machineOpenCount;
|
||||
|
||||
/** @var int */
|
||||
private $unsubscribeCount;
|
||||
|
||||
/** @var int */
|
||||
private $bounceCount;
|
||||
|
||||
/** @var int */
|
||||
private $totalSentCount;
|
||||
|
||||
/** @var WooCommerceRevenue|null */
|
||||
private $wooCommerceRevenue;
|
||||
|
||||
public function __construct(
|
||||
$clickCount,
|
||||
$openCount,
|
||||
$unsubscribeCount,
|
||||
$bounceCount,
|
||||
$totalSentCount,
|
||||
$wooCommerceRevenue
|
||||
) {
|
||||
$this->clickCount = $clickCount;
|
||||
$this->openCount = $openCount;
|
||||
$this->unsubscribeCount = $unsubscribeCount;
|
||||
$this->bounceCount = $bounceCount;
|
||||
$this->totalSentCount = $totalSentCount;
|
||||
$this->wooCommerceRevenue = $wooCommerceRevenue;
|
||||
}
|
||||
|
||||
public function getClickCount(): int {
|
||||
return $this->clickCount;
|
||||
}
|
||||
|
||||
public function getOpenCount(): int {
|
||||
return $this->openCount;
|
||||
}
|
||||
|
||||
public function getUnsubscribeCount(): int {
|
||||
return $this->unsubscribeCount;
|
||||
}
|
||||
|
||||
public function getBounceCount(): int {
|
||||
return $this->bounceCount;
|
||||
}
|
||||
|
||||
public function getTotalSentCount(): int {
|
||||
return $this->totalSentCount;
|
||||
}
|
||||
|
||||
public function getWooCommerceRevenue(): ?WooCommerceRevenue {
|
||||
return $this->wooCommerceRevenue;
|
||||
}
|
||||
|
||||
public function setMachineOpenCount(int $machineOpenCount): void {
|
||||
$this->machineOpenCount = $machineOpenCount;
|
||||
}
|
||||
|
||||
public function getMachineOpenCount(): int {
|
||||
return $this->machineOpenCount;
|
||||
}
|
||||
|
||||
public function asArray(): array {
|
||||
return [
|
||||
'clicked' => $this->clickCount,
|
||||
'opened' => $this->openCount,
|
||||
'machineOpened' => $this->machineOpenCount,
|
||||
'unsubscribed' => $this->unsubscribeCount,
|
||||
'bounced' => $this->bounceCount,
|
||||
'revenue' => empty($this->wooCommerceRevenue) ? null : $this->wooCommerceRevenue->asArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Statistics;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Repository;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\ScheduledTaskEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Entities\StatisticsBounceEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\StatisticsNewsletterEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\StatisticsUnsubscribeEntity;
|
||||
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\WooCommerce\Helper as WCHelper;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\UnexpectedResultException;
|
||||
|
||||
/**
|
||||
* @extends Repository<NewsletterEntity>
|
||||
*/
|
||||
class NewsletterStatisticsRepository extends Repository {
|
||||
|
||||
/** @var WCHelper */
|
||||
private $wcHelper;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
WCHelper $wcHelper,
|
||||
TrackingConfig $trackingConfig
|
||||
) {
|
||||
parent::__construct($entityManager);
|
||||
$this->wcHelper = $wcHelper;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
}
|
||||
|
||||
protected function getEntityClassName() {
|
||||
return NewsletterEntity::class;
|
||||
}
|
||||
|
||||
public function getStatistics(NewsletterEntity $newsletter): NewsletterStatistics {
|
||||
$stats = new NewsletterStatistics(
|
||||
$this->getStatisticsClickCount($newsletter),
|
||||
$this->getStatisticsOpenCount($newsletter),
|
||||
$this->getStatisticsUnsubscribeCount($newsletter),
|
||||
$this->getStatisticsBounceCount($newsletter),
|
||||
$this->getTotalSentCount($newsletter),
|
||||
$this->getWooCommerceRevenue($newsletter)
|
||||
);
|
||||
$stats->setMachineOpenCount($this->getStatisticsMachineOpenCount($newsletter));
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewsletterEntity[] $newsletters
|
||||
* @return NewsletterStatistics[]
|
||||
*/
|
||||
public function getBatchStatistics(
|
||||
array $newsletters,
|
||||
\DateTimeImmutable $from = null,
|
||||
\DateTimeImmutable $to = null,
|
||||
array $include = [
|
||||
'totals',
|
||||
StatisticsClickEntity::class,
|
||||
StatisticsOpenEntity::class,
|
||||
StatisticsUnsubscribeEntity::class,
|
||||
StatisticsBounceEntity::class,
|
||||
WooCommerceRevenue::class,
|
||||
]
|
||||
): array {
|
||||
|
||||
$totalSentCounts = in_array('totals', $include, true) ? $this->getTotalSentCounts($newsletters, $from, $to) : [];
|
||||
$clickCounts = in_array(StatisticsClickEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsClickEntity::class, $newsletters, $from, $to) : [];
|
||||
$openCounts = in_array(StatisticsOpenEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsOpenEntity::class, $newsletters, $from, $to) : [];
|
||||
$unsubscribeCounts = in_array(StatisticsUnsubscribeEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsUnsubscribeEntity::class, $newsletters, $from, $to) : [];
|
||||
$bounceCounts = in_array(StatisticsBounceEntity::class, $include, true) ? $this->getStatisticCounts(StatisticsBounceEntity::class, $newsletters, $from, $to) : [];
|
||||
$wooCommerceRevenues = in_array(WooCommerceRevenue::class, $include, true) ? $this->getWooCommerceRevenues($newsletters, $from, $to) : [];
|
||||
|
||||
$statistics = [];
|
||||
foreach ($newsletters as $newsletter) {
|
||||
$id = $newsletter->getId();
|
||||
$statistics[$id] = new NewsletterStatistics(
|
||||
$clickCounts[$id] ?? 0,
|
||||
$openCounts[$id] ?? 0,
|
||||
$unsubscribeCounts[$id] ?? 0,
|
||||
$bounceCounts[$id] ?? 0,
|
||||
$totalSentCounts[$id] ?? 0,
|
||||
$wooCommerceRevenues[$id] ?? null
|
||||
);
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function getTotalSentCount(NewsletterEntity $newsletter): int {
|
||||
$counts = $this->getTotalSentCounts([$newsletter]);
|
||||
return $counts[$newsletter->getId()] ?? 0;
|
||||
}
|
||||
|
||||
public function getStatisticsClickCount(NewsletterEntity $newsletter): int {
|
||||
$counts = $this->getStatisticCounts(StatisticsClickEntity::class, [$newsletter]);
|
||||
return $counts[$newsletter->getId()] ?? 0;
|
||||
}
|
||||
|
||||
public function getStatisticsOpenCount(NewsletterEntity $newsletter): int {
|
||||
$counts = $this->getStatisticCounts(StatisticsOpenEntity::class, [$newsletter]);
|
||||
return $counts[$newsletter->getId()] ?? 0;
|
||||
}
|
||||
|
||||
public function getStatisticsMachineOpenCount(NewsletterEntity $newsletter): int {
|
||||
$qb = $this->getStatisticsQuery(StatisticsOpenEntity::class, [$newsletter]);
|
||||
$result = $qb->andWhere('(stats.userAgentType = :userAgentType)')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
|
||||
if (empty($result)) return 0;
|
||||
return $result['cnt'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SubscriberEntity $subscriber
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return array(newsletter_id: string, newsletter_rendered_subject: string, opened_at: string|null, sent_at: string)
|
||||
*/
|
||||
public function getAllForSubscriber(
|
||||
SubscriberEntity $subscriber,
|
||||
int $limit = null,
|
||||
int $offset = null
|
||||
): array {
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('IDENTITY(statistics.newsletter) AS newsletter_id')
|
||||
->addSelect('opens.createdAt AS opened_at')
|
||||
->addSelect('queue.newsletterRenderedSubject AS newsletter_rendered_subject')
|
||||
->addSelect('statistics.sentAt AS sent_at')
|
||||
->from(StatisticsNewsletterEntity::class, 'statistics')
|
||||
->join(SendingQueueEntity::class, 'queue', Join::WITH, 'statistics.queue = queue')
|
||||
->leftJoin(
|
||||
StatisticsOpenEntity::class,
|
||||
'opens',
|
||||
Join::WITH,
|
||||
'statistics.newsletter = opens.newsletter AND statistics.subscriber = opens.subscriber'
|
||||
)
|
||||
->where('statistics.subscriber = :subscriber')
|
||||
->setParameter('subscriber', $subscriber)
|
||||
->addOrderBy('newsletter_id')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function getStatisticsUnsubscribeCount(NewsletterEntity $newsletter): int {
|
||||
$counts = $this->getStatisticCounts(StatisticsUnsubscribeEntity::class, [$newsletter]);
|
||||
return $counts[$newsletter->getId()] ?? 0;
|
||||
}
|
||||
|
||||
public function getStatisticsBounceCount(NewsletterEntity $newsletter): int {
|
||||
$counts = $this->getStatisticCounts(StatisticsBounceEntity::class, [$newsletter]);
|
||||
return $counts[$newsletter->getId()] ?? 0;
|
||||
}
|
||||
|
||||
public function getWooCommerceRevenue(NewsletterEntity $newsletter) {
|
||||
$revenues = $this->getWooCommerceRevenues([$newsletter]);
|
||||
return $revenues[$newsletter->getId()] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewsletterEntity $newsletter
|
||||
* @return int
|
||||
*/
|
||||
public function getChildrenCount(NewsletterEntity $newsletter) {
|
||||
try {
|
||||
return (int)$this->entityManager
|
||||
->createQueryBuilder()
|
||||
->select('COUNT(n.id) as cnt')
|
||||
->from(NewsletterEntity::class, 'n')
|
||||
->where('n.parent = :newsletter')
|
||||
->setParameter('newsletter', $newsletter)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
} catch (UnexpectedResultException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTotalSentCounts(array $newsletters, \DateTimeImmutable $from = null, \DateTimeImmutable $to = null): array {
|
||||
$query = $this->doctrineRepository
|
||||
->createQueryBuilder('n')
|
||||
->select('n.id, SUM(q.countProcessed) AS cnt')
|
||||
->join('n.queues', 'q')
|
||||
->join('q.task', 't')
|
||||
->where('t.status = :status')
|
||||
->setParameter('status', ScheduledTaskEntity::STATUS_COMPLETED)
|
||||
->andWhere('q.newsletter IN (:newsletters)')
|
||||
->setParameter('newsletters', $newsletters)
|
||||
->groupBy('n.id');
|
||||
|
||||
if ($from && $to) {
|
||||
$query->andWhere('q.createdAt BETWEEN :from AND :to')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to);
|
||||
} elseif ($from && $to === null) {
|
||||
$query->andWhere('q.createdAt >= :from')
|
||||
->setParameter('from', $from);
|
||||
} elseif ($from === null && $to) {
|
||||
$query->andWhere('q.createdAt <= :to')
|
||||
->setParameter('to', $to);
|
||||
}
|
||||
|
||||
$results = $query->getQuery()
|
||||
->getResult();
|
||||
|
||||
$counts = [];
|
||||
foreach ($results ?: [] as $result) {
|
||||
$counts[(int)$result['id']] = (int)$result['cnt'];
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
private function getStatisticCounts(string $statisticsEntityName, array $newsletters, \DateTimeImmutable $from = null, \DateTimeImmutable $to = null): array {
|
||||
$qb = $this->getStatisticsQuery($statisticsEntityName, $newsletters);
|
||||
if (
|
||||
$statisticsEntityName === StatisticsClickEntity::class
|
||||
|| ($statisticsEntityName === StatisticsOpenEntity::class && $this->trackingConfig->areOpensSeparated())
|
||||
) {
|
||||
$qb->andWhere('(stats.userAgentType = :userAgentType) OR (stats.userAgentType IS NULL)')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
|
||||
}
|
||||
if ($from && $to) {
|
||||
$qb->andWhere('stats.createdAt BETWEEN :from AND :to')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to);
|
||||
} elseif ($from && $to === null) {
|
||||
$qb->andWhere('stats.createdAt >= :from')
|
||||
->setParameter('from', $from);
|
||||
} elseif ($from === null && $to) {
|
||||
$qb->andWhere('stats.createdAt <= :to')
|
||||
->setParameter('to', $to);
|
||||
}
|
||||
|
||||
$results = $qb
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$counts = [];
|
||||
foreach ($results ?: [] as $result) {
|
||||
$counts[(int)$result['id']] = (int)$result['cnt'];
|
||||
}
|
||||
return $counts;
|
||||
}
|
||||
|
||||
private function getStatisticsQuery(string $statisticsEntityName, array $newsletters): QueryBuilder {
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('IDENTITY(stats.newsletter) AS id, COUNT(DISTINCT stats.subscriber) as cnt')
|
||||
->from($statisticsEntityName, 'stats')
|
||||
->where('stats.newsletter IN (:newsletters)')
|
||||
->groupBy('stats.newsletter')
|
||||
->setParameter('newsletters', $newsletters);
|
||||
}
|
||||
|
||||
private function getWooCommerceRevenues(array $newsletters, \DateTimeImmutable $from = null, \DateTimeImmutable $to = null) {
|
||||
if (!$this->wcHelper->isWooCommerceActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$revenueStatus = $this->wcHelper->getPurchaseStates();
|
||||
|
||||
$currency = $this->wcHelper->getWoocommerceCurrency();
|
||||
$query = $this->entityManager
|
||||
->createQueryBuilder()
|
||||
->select('IDENTITY(stats.newsletter) AS id, SUM(stats.orderPriceTotal) AS total, COUNT(stats.id) AS cnt')
|
||||
->from(StatisticsWooCommercePurchaseEntity::class, 'stats')
|
||||
->where('stats.newsletter IN (:newsletters)')
|
||||
->andWhere('stats.orderCurrency = :currency')
|
||||
->andWhere('stats.status IN (:revenue_status)')
|
||||
->setParameter('newsletters', $newsletters)
|
||||
->setParameter('currency', $currency)
|
||||
->setParameter('revenue_status', $revenueStatus)
|
||||
->groupBy('stats.newsletter');
|
||||
|
||||
if ($from && $to) {
|
||||
$query->andWhere('stats.createdAt BETWEEN :from AND :to')
|
||||
->setParameter('from', $from)
|
||||
->setParameter('to', $to);
|
||||
} elseif ($from && $to === null) {
|
||||
$query->andWhere('stats.createdAt >= :from')
|
||||
->setParameter('from', $from);
|
||||
} elseif ($from === null && $to) {
|
||||
$query->andWhere('stats.createdAt <= :to')
|
||||
->setParameter('to', $to);
|
||||
}
|
||||
|
||||
$results = $query->getQuery()
|
||||
->getResult();
|
||||
|
||||
$revenues = [];
|
||||
foreach ($results ?: [] as $result) {
|
||||
$revenues[(int)$result['id']] = new WooCommerceRevenue(
|
||||
$currency,
|
||||
(float)$result['total'],
|
||||
(int)$result['cnt'],
|
||||
$this->wcHelper
|
||||
);
|
||||
}
|
||||
return $revenues;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Statistics;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
|
||||
class WooCommerceRevenue {
|
||||
|
||||
/** @var string */
|
||||
private $currency;
|
||||
|
||||
/** @var float */
|
||||
private $value;
|
||||
|
||||
/** @var int */
|
||||
private $ordersCount;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooCommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
$currency,
|
||||
$value,
|
||||
$ordersCount,
|
||||
Helper $wooCommerceHelper
|
||||
) {
|
||||
$this->currency = $currency;
|
||||
$this->value = $value;
|
||||
$this->ordersCount = $ordersCount;
|
||||
$this->wooCommerceHelper = $wooCommerceHelper;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getCurrency() {
|
||||
return $this->currency;
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
public function getOrdersCount() {
|
||||
return $this->ordersCount;
|
||||
}
|
||||
|
||||
/** @return float */
|
||||
public function getValue() {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getFormattedValue() {
|
||||
return $this->wooCommerceHelper->getRawPrice($this->value, ['currency' => $this->currency]);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getFormattedAverageValue(): string {
|
||||
$average = 0;
|
||||
if ($this->ordersCount > 0) {
|
||||
$average = $this->value / $this->ordersCount;
|
||||
}
|
||||
return $this->wooCommerceHelper->getRawPrice($average, ['currency' => $this->currency]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function asArray() {
|
||||
return [
|
||||
'currency' => $this->currency,
|
||||
'value' => (float)$this->value,
|
||||
'count' => (int)$this->ordersCount,
|
||||
'formatted' => $this->getFormattedValue(),
|
||||
'formatted_average' => $this->getFormattedAverageValue(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user