We agreed that we can use WP translation functions directly. This one was probably some leftover which remained in the file after merging branches. The file was missing use statement so this was causing an error. [MAILPOET-4551]
410 lines
15 KiB
PHP
410 lines
15 KiB
PHP
<?php declare(strict_types = 1);
|
|
|
|
namespace MailPoet\Subscribers;
|
|
|
|
use MailPoet\Entities\SegmentEntity;
|
|
use MailPoet\Entities\SubscriberEntity;
|
|
use MailPoet\Entities\TagEntity;
|
|
use MailPoet\Listing\ListingDefinition;
|
|
use MailPoet\Listing\ListingRepository;
|
|
use MailPoet\Segments\DynamicSegments\FilterHandler;
|
|
use MailPoet\Segments\SegmentSubscribersRepository;
|
|
use MailPoet\Util\Helpers;
|
|
use MailPoetVendor\Doctrine\DBAL\Driver\Statement;
|
|
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder as DBALQueryBuilder;
|
|
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
|
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
|
|
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
|
|
|
class SubscriberListingRepository extends ListingRepository {
|
|
public const FILTER_WITHOUT_LIST = 'without-list';
|
|
|
|
const DEFAULT_SORT_BY = 'createdAt';
|
|
|
|
private static $supportedStatuses = [
|
|
SubscriberEntity::STATUS_SUBSCRIBED,
|
|
SubscriberEntity::STATUS_UNSUBSCRIBED,
|
|
SubscriberEntity::STATUS_INACTIVE,
|
|
SubscriberEntity::STATUS_BOUNCED,
|
|
SubscriberEntity::STATUS_UNCONFIRMED,
|
|
];
|
|
|
|
/** @var FilterHandler */
|
|
private $dynamicSegmentsFilter;
|
|
|
|
/** @var EntityManager */
|
|
private $entityManager;
|
|
|
|
/** @var SegmentSubscribersRepository */
|
|
private $segmentSubscribersRepository;
|
|
|
|
/** @var SubscribersCountsController */
|
|
private $subscribersCountsController;
|
|
|
|
public function __construct(
|
|
EntityManager $entityManager,
|
|
FilterHandler $dynamicSegmentsFilter,
|
|
SegmentSubscribersRepository $segmentSubscribersRepository,
|
|
SubscribersCountsController $subscribersCountsController
|
|
) {
|
|
parent::__construct($entityManager);
|
|
$this->dynamicSegmentsFilter = $dynamicSegmentsFilter;
|
|
$this->entityManager = $entityManager;
|
|
$this->segmentSubscribersRepository = $segmentSubscribersRepository;
|
|
$this->subscribersCountsController = $subscribersCountsController;
|
|
}
|
|
|
|
public function getData(ListingDefinition $definition): array {
|
|
$dynamicSegment = $this->getDynamicSegmentFromFilters($definition);
|
|
if ($dynamicSegment === null) {
|
|
return parent::getData($definition);
|
|
}
|
|
return $this->getDataForDynamicSegment($definition, $dynamicSegment);
|
|
}
|
|
|
|
public function getCount(ListingDefinition $definition): int {
|
|
$dynamicSegment = $this->getDynamicSegmentFromFilters($definition);
|
|
if ($dynamicSegment === null) {
|
|
return parent::getCount($definition);
|
|
}
|
|
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
|
$subscribersIdsQuery = $this->entityManager
|
|
->getConnection()
|
|
->createQueryBuilder()
|
|
->select("count(DISTINCT $subscribersTable.id)")
|
|
->from($subscribersTable);
|
|
$subscribersIdsQuery = $this->applyConstraintsForDynamicSegment($subscribersIdsQuery, $definition, $dynamicSegment);
|
|
return (int)$subscribersIdsQuery->execute()->fetchColumn();
|
|
}
|
|
|
|
public function getActionableIds(ListingDefinition $definition): array {
|
|
$ids = $definition->getSelection();
|
|
if (!empty($ids)) {
|
|
return $ids;
|
|
}
|
|
$dynamicSegment = $this->getDynamicSegmentFromFilters($definition);
|
|
if ($dynamicSegment === null) {
|
|
return parent::getActionableIds($definition);
|
|
}
|
|
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
|
$subscribersIdsQuery = $this->entityManager
|
|
->getConnection()
|
|
->createQueryBuilder()
|
|
->select("DISTINCT $subscribersTable.id")
|
|
->from($subscribersTable);
|
|
$subscribersIdsQuery = $this->applyConstraintsForDynamicSegment($subscribersIdsQuery, $definition, $dynamicSegment);
|
|
$idsStatement = $subscribersIdsQuery->execute();
|
|
$result = $idsStatement->fetchAll();
|
|
return array_column($result, 'id');
|
|
}
|
|
|
|
protected function applySelectClause(QueryBuilder $queryBuilder) {
|
|
$queryBuilder->select("PARTIAL s.{id,email,firstName,lastName,status,createdAt,countConfirmations,wpUserId,isWoocommerceUser,engagementScore}");
|
|
}
|
|
|
|
protected function applyFromClause(QueryBuilder $queryBuilder) {
|
|
$queryBuilder->from(SubscriberEntity::class, 's');
|
|
}
|
|
|
|
protected function applyGroup(QueryBuilder $queryBuilder, string $group) {
|
|
// include/exclude deleted
|
|
if ($group === 'trash') {
|
|
$queryBuilder->andWhere('s.deletedAt IS NOT NULL');
|
|
} else {
|
|
$queryBuilder->andWhere('s.deletedAt IS NULL');
|
|
}
|
|
|
|
if (!in_array($group, self::$supportedStatuses)) {
|
|
return;
|
|
}
|
|
|
|
$queryBuilder
|
|
->andWhere('s.status = :status')
|
|
->setParameter('status', $group);
|
|
}
|
|
|
|
protected function applySearch(QueryBuilder $queryBuilder, string $search) {
|
|
$search = Helpers::escapeSearch($search);
|
|
$queryBuilder
|
|
->andWhere('s.email LIKE :search or s.firstName LIKE :search or s.lastName LIKE :search')
|
|
->setParameter('search', "%$search%");
|
|
}
|
|
|
|
protected function applyFilters(QueryBuilder $queryBuilder, array $filters) {
|
|
if (isset($filters['segment'])) {
|
|
if ($filters['segment'] === self::FILTER_WITHOUT_LIST) {
|
|
$this->segmentSubscribersRepository->addConstraintsForSubscribersWithoutSegment($queryBuilder);
|
|
} else {
|
|
$segment = $this->entityManager->find(SegmentEntity::class, (int)$filters['segment']);
|
|
if ($segment instanceof SegmentEntity && $segment->isStatic()) {
|
|
$queryBuilder->join('s.subscriberSegments', 'ss', Join::WITH, 'ss.segment = :ssSegment')
|
|
->setParameter('ssSegment', $segment->getId());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($filters['tag'])) {
|
|
$tag = $this->entityManager->find(TagEntity::class, (int)$filters['tag']);
|
|
if ($tag) {
|
|
$queryBuilder->join('s.subscriberTags', 'st', Join::WITH, 'st.tag = :stTag')
|
|
->setParameter('stTag', $tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function applyParameters(QueryBuilder $queryBuilder, array $parameters) {
|
|
// nothing to do here
|
|
}
|
|
|
|
protected function applySorting(QueryBuilder $queryBuilder, string $sortBy, string $sortOrder) {
|
|
if (!$sortBy) {
|
|
$sortBy = self::DEFAULT_SORT_BY;
|
|
}
|
|
$queryBuilder->addOrderBy("s.$sortBy", $sortOrder);
|
|
}
|
|
|
|
public function getGroups(ListingDefinition $definition): array {
|
|
$queryBuilder = clone $this->queryBuilder;
|
|
$this->applyFromClause($queryBuilder);
|
|
|
|
$groupCounts = [
|
|
SubscriberEntity::STATUS_SUBSCRIBED => 0,
|
|
SubscriberEntity::STATUS_UNCONFIRMED => 0,
|
|
SubscriberEntity::STATUS_UNSUBSCRIBED => 0,
|
|
SubscriberEntity::STATUS_INACTIVE => 0,
|
|
SubscriberEntity::STATUS_BOUNCED => 0,
|
|
'trash' => 0,
|
|
];
|
|
foreach (array_keys($groupCounts) as $group) {
|
|
$groupDefinition = $group === $definition->getGroup() ? $definition : new ListingDefinition(
|
|
$group,
|
|
$definition->getFilters(),
|
|
$definition->getSearch(),
|
|
$definition->getParameters(),
|
|
$definition->getSortBy(),
|
|
$definition->getSortOrder(),
|
|
$definition->getOffset(),
|
|
$definition->getLimit(),
|
|
$definition->getSelection()
|
|
);
|
|
$groupCounts[$group] = $this->getCount($groupDefinition);
|
|
}
|
|
|
|
$trashedCount = $groupCounts['trash'];
|
|
unset($groupCounts['trash']);
|
|
$totalCount = (int)array_sum($groupCounts);
|
|
|
|
return [
|
|
[
|
|
'name' => 'all',
|
|
'label' => __('All', 'mailpoet'),
|
|
'count' => $totalCount,
|
|
],
|
|
[
|
|
'name' => SubscriberEntity::STATUS_SUBSCRIBED,
|
|
'label' => __('Subscribed', 'mailpoet'),
|
|
'count' => $groupCounts[SubscriberEntity::STATUS_SUBSCRIBED],
|
|
],
|
|
[
|
|
'name' => SubscriberEntity::STATUS_UNCONFIRMED,
|
|
'label' => __('Unconfirmed', 'mailpoet'),
|
|
'count' => $groupCounts[SubscriberEntity::STATUS_UNCONFIRMED],
|
|
],
|
|
[
|
|
'name' => SubscriberEntity::STATUS_UNSUBSCRIBED,
|
|
'label' => __('Unsubscribed', 'mailpoet'),
|
|
'count' => $groupCounts[SubscriberEntity::STATUS_UNSUBSCRIBED],
|
|
],
|
|
[
|
|
'name' => SubscriberEntity::STATUS_INACTIVE,
|
|
'label' => __('Inactive', 'mailpoet'),
|
|
'count' => $groupCounts[SubscriberEntity::STATUS_INACTIVE],
|
|
],
|
|
[
|
|
'name' => SubscriberEntity::STATUS_BOUNCED,
|
|
'label' => __('Bounced', 'mailpoet'),
|
|
'count' => $groupCounts[SubscriberEntity::STATUS_BOUNCED],
|
|
],
|
|
[
|
|
'name' => 'trash',
|
|
'label' => __('Trash', 'mailpoet'),
|
|
'count' => $trashedCount,
|
|
],
|
|
];
|
|
}
|
|
|
|
public function getFilters(ListingDefinition $definition): array {
|
|
return [
|
|
'segment' => $this->getSegmentFilter($definition),
|
|
'tag' => $this->getTagsFilter($definition),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<array{label: string, value: string|int}>
|
|
*/
|
|
private function getSegmentFilter(ListingDefinition $definition): array {
|
|
$group = $definition->getGroup();
|
|
|
|
$subscribersWithoutSegmentStats = $this->subscribersCountsController->getSubscribersWithoutSegmentStatisticsCount();
|
|
$key = $group ?: 'all';
|
|
$subscribersWithoutSegmentCount = $subscribersWithoutSegmentStats[$key];
|
|
|
|
$subscribersWithoutSegmentLabel = sprintf(
|
|
// translators: %s is the number of subscribers without a list.
|
|
__('Subscribers without a list (%s)', 'mailpoet'),
|
|
number_format((float)$subscribersWithoutSegmentCount)
|
|
);
|
|
|
|
$queryBuilder = clone $this->queryBuilder;
|
|
$queryBuilder
|
|
->select('s')
|
|
->from(SegmentEntity::class, 's');
|
|
if ($group === 'trash') {
|
|
$queryBuilder->andWhere('s.deletedAt IS NOT NULL');
|
|
} else {
|
|
$queryBuilder->andWhere('s.deletedAt IS NULL');
|
|
}
|
|
|
|
// format segment list
|
|
$allSubscribersList = [
|
|
'label' => __('All Lists', 'mailpoet'),
|
|
'value' => '',
|
|
];
|
|
|
|
$withoutSegmentList = [
|
|
'label' => $subscribersWithoutSegmentLabel,
|
|
'value' => self::FILTER_WITHOUT_LIST,
|
|
];
|
|
|
|
$segmentList = [];
|
|
foreach ($queryBuilder->getQuery()->getResult() as $segment) {
|
|
$key = $group ?: 'all';
|
|
if ($segment->isStatic()) {
|
|
$count = $this->subscribersCountsController->getSegmentGlobalStatusStatisticsCount($segment);
|
|
} else {
|
|
$count = $this->subscribersCountsController->getSegmentStatisticsCount($segment);
|
|
}
|
|
|
|
$segmentList[] = [
|
|
'label' => sprintf('%s (%s)', $segment->getName(), number_format((float)$count[$key])),
|
|
'value' => $segment->getId(),
|
|
];
|
|
}
|
|
|
|
usort($segmentList, function($a, $b) {
|
|
return strcasecmp($a['label'], $b['label']);
|
|
});
|
|
|
|
array_unshift($segmentList, $allSubscribersList, $withoutSegmentList);
|
|
return $segmentList;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{label: string, value: string|int}>
|
|
*/
|
|
private function getTagsFilter(ListingDefinition $definition): array {
|
|
$group = $definition->getGroup();
|
|
|
|
$allTagsList = [
|
|
'label' => __('All Tags', 'mailpoet'),
|
|
'value' => '',
|
|
];
|
|
|
|
$status = in_array($group, ['all', 'trash']) ? null : $group;
|
|
$isDeleted = $group === 'trash';
|
|
$tagsStatistics = $this->subscribersCountsController->getTagsStatisticsCount($status, $isDeleted);
|
|
|
|
$tagsList = [];
|
|
foreach ($tagsStatistics as $tagStatistics) {
|
|
$tagsList[] = [
|
|
'label' => sprintf('%s (%s)', $tagStatistics['name'], number_format((float)$tagStatistics['subscribersCount'])),
|
|
'value' => $tagStatistics['id'],
|
|
];
|
|
}
|
|
|
|
array_unshift($tagsList, $allTagsList);
|
|
return $tagsList;
|
|
}
|
|
|
|
private function getDataForDynamicSegment(ListingDefinition $definition, SegmentEntity $segment) {
|
|
$queryBuilder = clone $this->queryBuilder;
|
|
$sortBy = Helpers::underscoreToCamelCase($definition->getSortBy()) ?: self::DEFAULT_SORT_BY;
|
|
$this->applySelectClause($queryBuilder);
|
|
$this->applyFromClause($queryBuilder);
|
|
|
|
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
|
$subscribersIdsQuery = $this->entityManager
|
|
->getConnection()
|
|
->createQueryBuilder()
|
|
->select("DISTINCT $subscribersTable.id")
|
|
->from($subscribersTable);
|
|
$subscribersIdsQuery = $this->applyConstraintsForDynamicSegment($subscribersIdsQuery, $definition, $segment);
|
|
$subscribersIdsQuery->orderBy("$subscribersTable." . Helpers::camelCaseToUnderscore($sortBy), $definition->getSortOrder());
|
|
$subscribersIdsQuery->setFirstResult($definition->getOffset());
|
|
$subscribersIdsQuery->setMaxResults($definition->getLimit());
|
|
|
|
$idsStatement = $subscribersIdsQuery->execute();
|
|
// This shouldn't happen because execute on select SQL always returns Statement, but PHPStan doesn't know that
|
|
if (!$idsStatement instanceof Statement) {
|
|
$queryBuilder->andWhere('0 = 1');
|
|
return;
|
|
}
|
|
$result = $idsStatement->fetchAll();
|
|
$ids = array_column($result, 'id');
|
|
if (count($ids)) {
|
|
$queryBuilder->andWhere('s.id IN (:subscriberIds)')
|
|
->setParameter('subscriberIds', $ids);
|
|
} else {
|
|
$queryBuilder->andWhere('0 = 1'); // Don't return any subscribers if no ids found
|
|
}
|
|
$this->applySorting($queryBuilder, $sortBy, $definition->getSortOrder());
|
|
return $queryBuilder->getQuery()->getResult();
|
|
}
|
|
|
|
private function applyConstraintsForDynamicSegment(
|
|
DBALQueryBuilder $subscribersQuery,
|
|
ListingDefinition $definition,
|
|
SegmentEntity $segment
|
|
) {
|
|
// Apply dynamic segments filters
|
|
$subscribersQuery = $this->dynamicSegmentsFilter->apply($subscribersQuery, $segment);
|
|
// Apply group, search to fetch only necessary ids
|
|
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
|
if ($definition->getSearch()) {
|
|
$search = Helpers::escapeSearch((string)$definition->getSearch());
|
|
$subscribersQuery
|
|
->andWhere("$subscribersTable.email LIKE :search or $subscribersTable.first_name LIKE :search or $subscribersTable.last_name LIKE :search")
|
|
->setParameter('search', "%$search%");
|
|
}
|
|
if ($definition->getGroup()) {
|
|
if ($definition->getGroup() === 'trash') {
|
|
$subscribersQuery->andWhere("$subscribersTable.deleted_at IS NOT NULL");
|
|
} else {
|
|
$subscribersQuery->andWhere("$subscribersTable.deleted_at IS NULL");
|
|
}
|
|
if (in_array($definition->getGroup(), self::$supportedStatuses)) {
|
|
$subscribersQuery
|
|
->andWhere("$subscribersTable.status = :status")
|
|
->setParameter('status', $definition->getGroup());
|
|
}
|
|
}
|
|
return $subscribersQuery;
|
|
}
|
|
|
|
private function getDynamicSegmentFromFilters(ListingDefinition $definition): ?SegmentEntity {
|
|
$filters = $definition->getFilters();
|
|
if (!$filters || !isset($filters['segment'])) {
|
|
return null;
|
|
}
|
|
if ($filters['segment'] === self::FILTER_WITHOUT_LIST) {
|
|
return null;
|
|
}
|
|
$segment = $this->entityManager->find(SegmentEntity::class, (int)$filters['segment']);
|
|
if (!$segment instanceof SegmentEntity) {
|
|
return null;
|
|
}
|
|
return $segment->isStatic() ? null : $segment;
|
|
}
|
|
}
|