This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -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(),
];
}
}
@@ -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