We want to show admin how many subscribers subscribed to a list and how many unsubscribed within last 30 days so that they see the change. If someone subscribed and also unsubscribed within those 30 days we skip him. We don't have data to detect they were really subscribed at some point. [MAILPOET-4828]
500 lines
17 KiB
PHP
500 lines
17 KiB
PHP
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
|
|
|
namespace MailPoet\Subscribers;
|
|
|
|
use MailPoet\Config\SubscriberChangesNotifier;
|
|
use MailPoet\Doctrine\Repository;
|
|
use MailPoet\Entities\SegmentEntity;
|
|
use MailPoet\Entities\StatisticsUnsubscribeEntity;
|
|
use MailPoet\Entities\SubscriberCustomFieldEntity;
|
|
use MailPoet\Entities\SubscriberEntity;
|
|
use MailPoet\Entities\SubscriberSegmentEntity;
|
|
use MailPoet\Entities\SubscriberTagEntity;
|
|
use MailPoet\Util\License\Features\Subscribers;
|
|
use MailPoet\WP\Functions as WPFunctions;
|
|
use MailPoetVendor\Carbon\Carbon;
|
|
use MailPoetVendor\Carbon\CarbonImmutable;
|
|
use MailPoetVendor\Doctrine\DBAL\Connection;
|
|
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
|
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
|
|
|
|
/**
|
|
* @extends Repository<SubscriberEntity>
|
|
*/
|
|
class SubscribersRepository extends Repository {
|
|
/** @var WPFunctions */
|
|
private $wp;
|
|
|
|
protected $ignoreColumnsForUpdate = [
|
|
'wp_user_id',
|
|
'is_woocommerce_user',
|
|
'email',
|
|
'created_at',
|
|
'last_subscribed_at',
|
|
];
|
|
|
|
/** @var SubscriberChangesNotifier */
|
|
private $changesNotifier;
|
|
|
|
public function __construct(
|
|
EntityManager $entityManager,
|
|
SubscriberChangesNotifier $changesNotifier,
|
|
WPFunctions $wp
|
|
) {
|
|
$this->wp = $wp;
|
|
parent::__construct($entityManager);
|
|
$this->changesNotifier = $changesNotifier;
|
|
}
|
|
|
|
protected function getEntityClassName() {
|
|
return SubscriberEntity::class;
|
|
}
|
|
|
|
public function getTotalSubscribers(): int {
|
|
return $this->getCountOfSubscribersForStates([
|
|
SubscriberEntity::STATUS_SUBSCRIBED,
|
|
SubscriberEntity::STATUS_UNCONFIRMED,
|
|
SubscriberEntity::STATUS_INACTIVE,
|
|
]);
|
|
}
|
|
|
|
public function getCountOfSubscribersForStates(array $states): int {
|
|
$query = $this->entityManager
|
|
->createQueryBuilder()
|
|
->select('count(n.id)')
|
|
->from(SubscriberEntity::class, 'n')
|
|
->where('n.deletedAt IS NULL AND n.status IN (:statuses)')
|
|
->setParameter('statuses', $states)
|
|
->getQuery();
|
|
return intval($query->getSingleScalarResult());
|
|
}
|
|
|
|
public function invalidateTotalSubscribersCache(): void {
|
|
$this->wp->deleteTransient(Subscribers::SUBSCRIBERS_COUNT_CACHE_KEY);
|
|
}
|
|
|
|
public function findBySegment(int $segmentId): array {
|
|
return $this->entityManager
|
|
->createQueryBuilder()
|
|
->select('s')
|
|
->from(SubscriberEntity::class, 's')
|
|
->join('s.subscriberSegments', 'ss', Join::WITH, 'ss.segment = :segment')
|
|
->setParameter('segment', $segmentId)
|
|
->getQuery()->getResult();
|
|
}
|
|
|
|
public function findExclusiveSubscribersBySegment(int $segmentId): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s')
|
|
->from(SubscriberEntity::class, 's')
|
|
->join('s.subscriberSegments', 'ss', Join::WITH, 'ss.segment = :segment')
|
|
->leftJoin('s.subscriberSegments', 'ss2', Join::WITH, 'ss2.segment <> :segment AND ss2.status = :subscribed')
|
|
->leftJoin('ss2.segment', 'seg', Join::WITH, 'seg.deletedAt IS NULL')
|
|
->groupBy('s.id')
|
|
->andHaving('COUNT(seg.id) = 0')
|
|
->setParameter('segment', $segmentId)
|
|
->setParameter('subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
|
->getQuery()->getResult();
|
|
}
|
|
|
|
public function getWooCommerceSegmentSubscriber(string $email): ?SubscriberEntity {
|
|
$subscriber = $this->doctrineRepository->createQueryBuilder('s')
|
|
->join('s.subscriberSegments', 'ss')
|
|
->join('ss.segment', 'sg', Join::WITH, 'sg.type = :typeWcUsers')
|
|
->where('s.isWoocommerceUser = 1')
|
|
->andWhere('s.status IN (:subscribed, :unconfirmed)')
|
|
->andWhere('ss.status = :subscribed')
|
|
->andWhere('s.email = :email')
|
|
->setParameter('typeWcUsers', SegmentEntity::TYPE_WC_USERS)
|
|
->setParameter('subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
|
->setParameter('unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
|
|
->setParameter('email', $email)
|
|
->setMaxResults(1)
|
|
->getQuery()
|
|
->getOneOrNullResult();
|
|
return $subscriber instanceof SubscriberEntity ? $subscriber : null;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkTrash(array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$this->entityManager->createQueryBuilder()
|
|
->update(SubscriberEntity::class, 's')
|
|
->set('s.deletedAt', 'CURRENT_TIMESTAMP()')
|
|
->where('s.id IN (:ids)')
|
|
->setParameter('ids', $ids)
|
|
->getQuery()->execute();
|
|
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
$this->invalidateTotalSubscribersCache();
|
|
return count($ids);
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkRestore(array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$this->entityManager->createQueryBuilder()
|
|
->update(SubscriberEntity::class, 's')
|
|
->set('s.deletedAt', ':deletedAt')
|
|
->where('s.id IN (:ids)')
|
|
->setParameter('deletedAt', null)
|
|
->setParameter('ids', $ids)
|
|
->getQuery()->execute();
|
|
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
$this->invalidateTotalSubscribersCache();
|
|
return count($ids);
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkDelete(array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$count = 0;
|
|
$this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, &$count) {
|
|
// Delete subscriber segments
|
|
$this->removeSubscribersFromAllSegments($ids);
|
|
|
|
// Delete subscriber custom fields
|
|
$subscriberCustomFieldTable = $entityManager->getClassMetadata(SubscriberCustomFieldEntity::class)->getTableName();
|
|
$subscriberTable = $entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
|
$entityManager->getConnection()->executeStatement("
|
|
DELETE scs FROM $subscriberCustomFieldTable scs
|
|
JOIN $subscriberTable s ON s.`id` = scs.`subscriber_id`
|
|
WHERE scs.`subscriber_id` IN (:ids)
|
|
AND s.`is_woocommerce_user` = false
|
|
AND s.`wp_user_id` IS NULL
|
|
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
// Delete subscriber tags
|
|
$subscriberTagTable = $entityManager->getClassMetadata(SubscriberTagEntity::class)->getTableName();
|
|
$entityManager->getConnection()->executeStatement("
|
|
DELETE st FROM $subscriberTagTable st
|
|
JOIN $subscriberTable s ON s.`id` = st.`subscriber_id`
|
|
WHERE st.`subscriber_id` IN (:ids)
|
|
AND s.`is_woocommerce_user` = false
|
|
AND s.`wp_user_id` IS NULL
|
|
", ['ids' => $ids], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
$queryBuilder = $entityManager->createQueryBuilder();
|
|
$count = $queryBuilder->delete(SubscriberEntity::class, 's')
|
|
->where('s.id IN (:ids)')
|
|
->andWhere('s.wpUserId IS NULL')
|
|
->andWhere('s.isWoocommerceUser = false')
|
|
->setParameter('ids', $ids)
|
|
->getQuery()->execute();
|
|
});
|
|
|
|
$this->changesNotifier->subscribersDeleted($ids);
|
|
$this->invalidateTotalSubscribersCache();
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkRemoveFromSegment(SegmentEntity $segment, array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
|
$count = (int)$this->entityManager->getConnection()->executeStatement("
|
|
DELETE ss FROM $subscriberSegmentsTable ss
|
|
WHERE ss.`subscriber_id` IN (:ids)
|
|
AND ss.`segment_id` = :segment_id
|
|
", ['ids' => $ids, 'segment_id' => $segment->getId()], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkRemoveFromAllSegments(array $ids): int {
|
|
$count = $this->removeSubscribersFromAllSegments($ids);
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkAddToSegment(SegmentEntity $segment, array $ids): int {
|
|
$count = $this->addSubscribersToSegment($segment, $ids);
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
return $count;
|
|
}
|
|
|
|
public function woocommerceUserExists(): bool {
|
|
$subscribers = $this->entityManager
|
|
->createQueryBuilder()
|
|
->select('s')
|
|
->from(SubscriberEntity::class, 's')
|
|
->join('s.subscriberSegments', 'ss')
|
|
->join('ss.segment', 'segment')
|
|
->where('segment.type = :segmentType')
|
|
->setParameter('segmentType', SegmentEntity::TYPE_WC_USERS)
|
|
->andWhere('s.isWoocommerceUser = true')
|
|
->getQuery()
|
|
->setMaxResults(1)
|
|
->execute();
|
|
|
|
return count($subscribers) > 0;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
public function bulkMoveToSegment(SegmentEntity $segment, array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$this->removeSubscribersFromAllSegments($ids);
|
|
$count = $this->addSubscribersToSegment($segment, $ids);
|
|
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
return $count;
|
|
}
|
|
|
|
public function bulkUnsubscribe(array $ids): int {
|
|
$this->entityManager->createQueryBuilder()
|
|
->update(SubscriberEntity::class, 's')
|
|
->set('s.status', ':status')
|
|
->where('s.id IN (:ids)')
|
|
->setParameter('status', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
|
->setParameter('ids', $ids)
|
|
->getQuery()->execute();
|
|
|
|
$this->changesNotifier->subscribersUpdated($ids);
|
|
$this->invalidateTotalSubscribersCache();
|
|
return count($ids);
|
|
}
|
|
|
|
public function findWpUserIdAndEmailByEmails(array $emails): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s.wpUserId AS wp_user_id, LOWER(s.email) AS email')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.email IN (:emails)')
|
|
->setParameter('emails', $emails)
|
|
->getQuery()->getResult();
|
|
}
|
|
|
|
public function findIdAndEmailByEmails(array $emails): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s.id, s.email')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.email IN (:emails)')
|
|
->setParameter('emails', $emails)
|
|
->getQuery()->getResult();
|
|
}
|
|
|
|
/**
|
|
* @return int[]
|
|
*/
|
|
public function findIdsOfDeletedByEmails(array $emails): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s.id')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.email IN (:emails)')
|
|
->andWhere('s.deletedAt IS NOT NULL')
|
|
->setParameter('emails', $emails)
|
|
->getQuery()->getResult();
|
|
}
|
|
|
|
public function getCurrentWPUser(): ?SubscriberEntity {
|
|
$wpUser = WPFunctions::get()->wpGetCurrentUser();
|
|
if (empty($wpUser->ID)) {
|
|
return null; // Don't look up a subscriber for guests
|
|
}
|
|
return $this->findOneBy(['wpUserId' => $wpUser->ID]);
|
|
}
|
|
|
|
public function findByUpdatedScoreNotInLastMonth(int $limit): array {
|
|
$dateTime = (new Carbon())->subMonths(1);
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.engagementScoreUpdatedAt IS NULL')
|
|
->orWhere('s.engagementScoreUpdatedAt < :dateTime')
|
|
->setParameter('dateTime', $dateTime)
|
|
->getQuery()
|
|
->setMaxResults($limit)
|
|
->getResult();
|
|
}
|
|
|
|
public function maybeUpdateLastEngagement(SubscriberEntity $subscriberEntity): void {
|
|
$now = CarbonImmutable::createFromTimestamp((int)$this->wp->currentTime('timestamp'));
|
|
// Do not update engagement if was recently updated to avoid unnecessary updates in DB
|
|
if ($subscriberEntity->getLastEngagementAt() && $subscriberEntity->getLastEngagementAt() > $now->subMinute()) {
|
|
return;
|
|
}
|
|
// Update last engagement
|
|
$subscriberEntity->setLastEngagementAt($now);
|
|
$this->flush();
|
|
}
|
|
|
|
/**
|
|
* @param array $ids
|
|
* @return string[]
|
|
*/
|
|
public function getUndeletedSubscribersEmailsByIds(array $ids): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('s.email')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.deletedAt IS NULL')
|
|
->andWhere('s.id IN (:ids)')
|
|
->setParameter('ids', $ids)
|
|
->getQuery()
|
|
->getArrayResult();
|
|
}
|
|
|
|
public function getMaxSubscriberId(): int {
|
|
$maxSubscriberId = $this->entityManager->createQueryBuilder()
|
|
->select('MAX(s.id)')
|
|
->from(SubscriberEntity::class, 's')
|
|
->getQuery()
|
|
->getSingleScalarResult();
|
|
|
|
return is_int($maxSubscriberId) ? $maxSubscriberId : 0;
|
|
}
|
|
|
|
public function getCountOfCreatedAfterWithStatues(\DateTimeInterface $createdAfter, array $statuses): int {
|
|
$result = $this->entityManager->createQueryBuilder()
|
|
->select('COUNT(s.id)')
|
|
->from(SubscriberEntity::class, 's')
|
|
->where('s.createdAt > :createdAfter')
|
|
->andWhere('s.status IN (:statuses)')
|
|
->andWhere('s.deletedAt IS NULL')
|
|
->setParameter('createdAfter', $createdAfter)
|
|
->setParameter('statuses', $statuses)
|
|
->getQuery()
|
|
->getSingleScalarResult();
|
|
return intval($result);
|
|
}
|
|
|
|
public function getCountOfUnsubscribedAfter(\DateTimeInterface $unsubscribedAfter): int {
|
|
$result = $this->entityManager->createQueryBuilder()
|
|
->select('COUNT(DISTINCT s.id)')
|
|
->from(StatisticsUnsubscribeEntity::class, 'su')
|
|
->join('su.subscriber', 's')
|
|
->where('s.createdAt <= :unsubscribedAfter')
|
|
->andWhere('su.createdAt > :unsubscribedAfter')
|
|
->andWhere('s.status = :status')
|
|
->andWhere('s.deletedAt IS NULL')
|
|
->setParameter('unsubscribedAfter', $unsubscribedAfter)
|
|
->setParameter('status', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
|
->getQuery()
|
|
->getSingleScalarResult();
|
|
return intval($result);
|
|
}
|
|
|
|
public function getListLevelCountsOfSubscribedAfter(\DateTimeInterface $date): array {
|
|
$data = $this->entityManager->createQueryBuilder()
|
|
->select('seg.id, seg.name, seg.type, seg.averageEngagementScore, COUNT(ss.id) as count')
|
|
->from(SubscriberSegmentEntity::class, 'ss')
|
|
->join('ss.subscriber', 's')
|
|
->join('ss.segment', 'seg')
|
|
->where('ss.createdAt > :date')
|
|
->andWhere('s.status = :status')
|
|
->andWhere('ss.status = :segment_status')
|
|
->andWhere('s.deletedAt IS NULL')
|
|
->andWhere('seg.deletedAt IS NULL') // no trashed lists and disabled WP Users list
|
|
->setParameter('date', $date)
|
|
->setParameter('status', SubscriberEntity::STATUS_SUBSCRIBED)
|
|
->setParameter('segment_status', SubscriberEntity::STATUS_SUBSCRIBED)
|
|
->groupBy('ss.segment')
|
|
->getQuery()
|
|
->getArrayResult();
|
|
return $data;
|
|
}
|
|
|
|
public function getListLevelCountsOfUnsubscribedAfter(\DateTimeInterface $date): array {
|
|
return $this->entityManager->createQueryBuilder()
|
|
->select('seg.id, seg.name, seg.type, seg.averageEngagementScore, COUNT(ss.id) as count')
|
|
->from(SubscriberSegmentEntity::class, 'ss')
|
|
->join('ss.subscriber', 's')
|
|
->join('ss.segment', 'seg')
|
|
->where('ss.updatedAt > :date')
|
|
->where('ss.createdAt < :date') // ignore those who subscribed and unsubscribed within the date range
|
|
->andWhere('s.status = :status')
|
|
->andWhere('ss.status = :segment_status')
|
|
->andWhere('s.deletedAt IS NULL')
|
|
->andWhere('seg.deletedAt IS NULL') // no trashed lists and disabled WP Users list
|
|
->setParameter('date', $date)
|
|
->setParameter('status', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
|
->setParameter('segment_status', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
|
->groupBy('ss.segment')
|
|
->getQuery()
|
|
->getArrayResult();
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
private function removeSubscribersFromAllSegments(array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
|
$segmentsTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
|
$count = (int)$this->entityManager->getConnection()->executeStatement("
|
|
DELETE ss FROM $subscriberSegmentsTable ss
|
|
JOIN $segmentsTable s ON s.id = ss.segment_id AND s.`type` = :typeDefault
|
|
WHERE ss.`subscriber_id` IN (:ids)
|
|
", [
|
|
'ids' => $ids,
|
|
'typeDefault' => SegmentEntity::TYPE_DEFAULT,
|
|
], ['ids' => Connection::PARAM_INT_ARRAY]);
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @return int - number of processed ids
|
|
*/
|
|
private function addSubscribersToSegment(SegmentEntity $segment, array $ids): int {
|
|
if (empty($ids)) {
|
|
return 0;
|
|
}
|
|
|
|
$subscribers = $this->entityManager
|
|
->createQueryBuilder()
|
|
->select('s')
|
|
->from(SubscriberEntity::class, 's')
|
|
->leftJoin('s.subscriberSegments', 'ss', Join::WITH, 'ss.segment = :segment')
|
|
->where('s.id IN (:ids)')
|
|
->andWhere('ss.segment IS NULL')
|
|
->setParameter('ids', $ids)
|
|
->setParameter('segment', $segment)
|
|
->getQuery()->execute();
|
|
|
|
$this->entityManager->transactional(function (EntityManager $entityManager) use ($subscribers, $segment) {
|
|
foreach ($subscribers as $subscriber) {
|
|
$subscriberSegment = new SubscriberSegmentEntity($segment, $subscriber, SubscriberEntity::STATUS_SUBSCRIBED);
|
|
$this->entityManager->persist($subscriberSegment);
|
|
}
|
|
$this->entityManager->flush();
|
|
});
|
|
|
|
return count($subscribers);
|
|
}
|
|
}
|