init
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Repository;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @extends Repository<DynamicSegmentFilterEntity>
|
||||
*/
|
||||
class DynamicSegmentFilterRepository extends Repository {
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager);
|
||||
}
|
||||
|
||||
protected function getEntityClassName() {
|
||||
return DynamicSegmentFilterEntity::class;
|
||||
}
|
||||
|
||||
public function findOnyByFilterTypeAndAction(string $filterType, string $action): ?DynamicSegmentFilterEntity {
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('dsf')
|
||||
->from(DynamicSegmentFilterEntity::class, 'dsf')
|
||||
->where('dsf.filterData.filterType = :filterType')
|
||||
->andWhere('dsf.filterData.action = :action')
|
||||
->setParameter('filterType', $filterType)
|
||||
->setParameter('action', $action)
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Segments\SegmentListingRepository;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class DynamicSegmentsListingRepository extends SegmentListingRepository {
|
||||
protected function applyParameters(QueryBuilder $queryBuilder, array $parameters): void {
|
||||
$queryBuilder
|
||||
->andWhere('s.type = :type')
|
||||
->setParameter('type', SegmentEntity::TYPE_DYNAMIC);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\InvalidStateException;
|
||||
|
||||
class InvalidFilterException extends InvalidStateException {
|
||||
const MISSING_TYPE = 1;
|
||||
const INVALID_TYPE = 2;
|
||||
const MISSING_ROLE = 3;
|
||||
const MISSING_ACTION = 4;
|
||||
const MISSING_NEWSLETTER_ID = 5;
|
||||
const MISSING_CATEGORY_ID = 6;
|
||||
const MISSING_PRODUCT_ID = 7;
|
||||
const INVALID_EMAIL_ACTION = 8;
|
||||
const MISSING_VALUE = 9;
|
||||
const MISSING_NUMBER_OF_ORDERS_FIELDS = 10;
|
||||
const MISSING_TOTAL_SPENT_FIELDS = 11;
|
||||
const INVALID_DATE_VALUE = 12;
|
||||
const MISSING_COUNTRY = 13;
|
||||
const MISSING_FILTER = 14;
|
||||
const MISSING_OPERATOR = 15;
|
||||
const MISSING_PLAN_ID = 16;
|
||||
const MISSING_SINGLE_ORDER_VALUE_FIELDS = 17;
|
||||
const MISSING_AVERAGE_SPENT_FIELDS = 18;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,591 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\AutomationsEvents;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailsReceived;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\FilterHelper;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\NumberOfClicks;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedViaForm;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceFirstOrder;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedCouponCode;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedShippingMethod;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class FilterDataMapper {
|
||||
private WPFunctions $wp;
|
||||
|
||||
private DateFilterHelper $dateFilterHelper;
|
||||
|
||||
private WooCommerceNumberOfReviews $wooCommerceNumberOfReviews;
|
||||
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
private WooCommerceUsedCouponCode $wooCommerceUsedCouponCode;
|
||||
|
||||
private WooCommerceTag $wooCommerceTag;
|
||||
|
||||
private WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp,
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooCommerceNumberOfReviews $wooCommerceNumberOfReviews,
|
||||
WooCommerceUsedCouponCode $wooCommerceUsedCouponCode,
|
||||
WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute,
|
||||
WooCommerceTag $wooCommerceTag
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews;
|
||||
$this->wooCommerceUsedCouponCode = $wooCommerceUsedCouponCode;
|
||||
$this->wooCommercePurchasedWithAttribute = $wooCommercePurchasedWithAttribute;
|
||||
$this->wooCommerceTag = $wooCommerceTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return DynamicSegmentFilterData[]
|
||||
*/
|
||||
public function map(array $data = []): array {
|
||||
if (!isset($data['filters']) || count($data['filters'] ?? []) < 1) {
|
||||
throw new InvalidFilterException('Filters are missing', InvalidFilterException::MISSING_FILTER);
|
||||
}
|
||||
$processFilter = function ($filter, $data) {
|
||||
$filter['connect'] = $data['filters_connect'] ?? DynamicSegmentFilterData::CONNECT_TYPE_AND;
|
||||
return $this->createFilter($filter);
|
||||
};
|
||||
$wpFilterName = 'mailpoet_dynamic_segments_filters_map';
|
||||
if ($this->wp->hasFilter($wpFilterName)) {
|
||||
return $this->wp->applyFilters($wpFilterName, $data, $processFilter);
|
||||
}
|
||||
$filter = reset($data['filters']);
|
||||
return [$processFilter($filter, $data)];
|
||||
}
|
||||
|
||||
private function createFilter(array $filterData): DynamicSegmentFilterData {
|
||||
if (isset($filterData['days']) && !isset($filterData['timeframe'])) {
|
||||
// Backwards compatibility for filters created before time period component had "over all time" option
|
||||
$filterData['timeframe'] = DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST;
|
||||
}
|
||||
switch ($this->getSegmentType($filterData)) {
|
||||
case DynamicSegmentFilterData::TYPE_AUTOMATIONS:
|
||||
return $this->createAutomations($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_USER_ROLE:
|
||||
return $this->createSubscriber($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_EMAIL:
|
||||
return $this->createEmail($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE:
|
||||
return $this->createWooCommerce($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP:
|
||||
return $this->createWooCommerceMembership($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION:
|
||||
return $this->createWooCommerceSubscription($filterData);
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function getSegmentType(array $data): string {
|
||||
if (!isset($data['segmentType'])) {
|
||||
throw new InvalidFilterException('Segment type is not set', InvalidFilterException::MISSING_TYPE);
|
||||
}
|
||||
return $data['segmentType'];
|
||||
}
|
||||
|
||||
private function createAutomations(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing automations filter action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (in_array($data['action'], AutomationsEvents::SUPPORTED_ACTIONS)) {
|
||||
if (
|
||||
!isset($data['operator']) || !in_array($data['operator'], [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (
|
||||
!isset($data['automation_ids'])
|
||||
|| !is_array($data['automation_ids'])
|
||||
|| count($data['automation_ids']) < 1
|
||||
) {
|
||||
throw new InvalidFilterException('Missing automation IDs', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_AUTOMATIONS, $data['action'], [
|
||||
'action' => $data['action'],
|
||||
'automation_ids' => $data['automation_ids'],
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Unknown automations action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
private function createSubscriber(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
$data['action'] = DynamicSegmentFilterData::TYPE_USER_ROLE;
|
||||
}
|
||||
if ($data['action'] === SubscriberScore::TYPE) {
|
||||
if (!isset($data['value'])) {
|
||||
throw new InvalidFilterException('Missing engagement score value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'] ?? SubscriberScore::HIGHER_THAN,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === SubscriberSegment::TYPE) {
|
||||
if (empty($data['segments'])) {
|
||||
throw new InvalidFilterException('Missing segments', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'segments' => array_map(function ($segmentId) {
|
||||
return intval($segmentId);
|
||||
}, $data['segments']),
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === MailPoetCustomFields::TYPE) {
|
||||
if (empty($data['custom_field_id'])) {
|
||||
throw new InvalidFilterException('Missing custom field id', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['custom_field_type'])) {
|
||||
throw new InvalidFilterException('Missing custom field type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData = [
|
||||
'value' => $data['value'],
|
||||
'custom_field_id' => $data['custom_field_id'],
|
||||
'custom_field_type' => $data['custom_field_type'],
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
if (!empty($data['date_type'])) {
|
||||
$filterData['date_type'] = $data['date_type'];
|
||||
}
|
||||
if (!empty($data['operator'])) {
|
||||
$filterData['operator'] = $data['operator'];
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], $filterData);
|
||||
}
|
||||
if ($data['action'] === SubscriberTag::TYPE) {
|
||||
if (empty($data['tags'])) {
|
||||
throw new InvalidFilterException('Missing tags', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'tags' => array_map(function ($tagId) {
|
||||
return intval($tagId);
|
||||
}, $data['tags']),
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === SubscriberSubscribedViaForm::TYPE) {
|
||||
if (!isset($data['form_ids']) || empty($data['form_ids'])) {
|
||||
throw new InvalidFilterException('Missing at least one form ID', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], [DynamicSegmentFilterData::OPERATOR_ANY, DynamicSegmentFilterData::OPERATOR_NONE])) {
|
||||
throw new InvalidFilterException('Missing valid operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'form_ids' => array_map(function($formId) {
|
||||
return intval($formId);
|
||||
}, $data['form_ids']),
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (in_array($data['action'], SubscriberTextField::TYPES)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'],
|
||||
'action' => $data['action'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (in_array($data['action'], SubscriberDateField::TYPES)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing date value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], $this->dateFilterHelper->getValidOperators())) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (empty($data['wordpressRole'])) {
|
||||
throw new InvalidFilterException('Missing role', InvalidFilterException::MISSING_ROLE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'wordpressRole' => $data['wordpressRole'],
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createEmail(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
if (!in_array($data['action'], EmailAction::ALLOWED_ACTIONS)) {
|
||||
throw new InvalidFilterException('Invalid email action', InvalidFilterException::INVALID_EMAIL_ACTION);
|
||||
}
|
||||
if (
|
||||
($data['action'] === EmailOpensAbsoluteCountAction::TYPE)
|
||||
|| ($data['action'] === EmailOpensAbsoluteCountAction::MACHINE_TYPE)
|
||||
) {
|
||||
return $this->createEmailOpensAbsoluteCount($data);
|
||||
}
|
||||
if ($data['action'] === EmailActionClickAny::TYPE) {
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], [
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
];
|
||||
|
||||
if ($data['action'] === EmailsReceived::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (!isset($data['emails'])) {
|
||||
throw new InvalidFilterException('Missing email count value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData['emails'] = $data['emails'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['connect'] = $data['connect'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
|
||||
}
|
||||
|
||||
if ($data['action'] === NumberOfClicks::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (!isset($data['clicks'])) {
|
||||
throw new InvalidFilterException('Missing click count value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData['clicks'] = $data['clicks'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['connect'] = $data['connect'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
|
||||
}
|
||||
|
||||
if (($data['action'] === EmailAction::ACTION_CLICKED)) {
|
||||
if (empty($data['newsletter_id'])) {
|
||||
throw new InvalidFilterException('Missing newsletter id', InvalidFilterException::MISSING_NEWSLETTER_ID);
|
||||
}
|
||||
$filterData['newsletter_id'] = $data['newsletter_id'];
|
||||
} else {
|
||||
if (empty($data['newsletters']) || !is_array($data['newsletters'])) {
|
||||
throw new InvalidFilterException('Missing newsletter', InvalidFilterException::MISSING_NEWSLETTER_ID);
|
||||
}
|
||||
$filterData['newsletters'] = array_map(function ($segmentId) {
|
||||
return intval($segmentId);
|
||||
}, $data['newsletters']);
|
||||
}
|
||||
|
||||
$filterType = DynamicSegmentFilterData::TYPE_EMAIL;
|
||||
$action = $data['action'];
|
||||
if (isset($data['link_ids']) && is_array($data['link_ids'])) {
|
||||
$filterData['link_ids'] = array_map('intval', $data['link_ids']);
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createEmailOpensAbsoluteCount(array $data): DynamicSegmentFilterData {
|
||||
if (!isset($data['opens'])) {
|
||||
throw new InvalidFilterException('Missing number of opens', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData = [
|
||||
'opens' => $data['opens'],
|
||||
'days' => $data['days'] ?? 0,
|
||||
'operator' => $data['operator'] ?? 'more',
|
||||
'timeframe' => $data['timeframe'] ?? DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST, // backwards compatibility
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_EMAIL;
|
||||
$action = $data['action'];
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerce(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceCategory::ACTION_CATEGORY) {
|
||||
if (!isset($data['category_ids'])) {
|
||||
throw new InvalidFilterException('Missing category', InvalidFilterException::MISSING_CATEGORY_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['category_ids'] = $data['category_ids'];
|
||||
} elseif ($data['action'] === WooCommerceProduct::ACTION_PRODUCT) {
|
||||
if (!isset($data['product_ids'])) {
|
||||
throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['product_ids'] = $data['product_ids'];
|
||||
} elseif ($data['action'] === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) {
|
||||
if (!isset($data['country_code'])) {
|
||||
throw new InvalidFilterException('Missing country', InvalidFilterException::MISSING_COUNTRY);
|
||||
}
|
||||
$filterData['country_code'] = $data['country_code'];
|
||||
$filterData['operator'] = $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
} elseif (in_array($data['action'], WooCommerceNumberOfOrders::ACTIONS)) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['number_of_orders_type'])
|
||||
|| !isset($data['number_of_orders_count']) || $data['number_of_orders_count'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS);
|
||||
}
|
||||
$filterData['number_of_orders_type'] = $data['number_of_orders_type'];
|
||||
$filterData['number_of_orders_count'] = $data['number_of_orders_count'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceNumberOfReviews::ACTION) {
|
||||
$this->wooCommerceNumberOfReviews->validateFilterData($data);
|
||||
$filterData['days'] = $data['days'];
|
||||
$filterData['count_type'] = $data['count_type'];
|
||||
$filterData['count'] = $data['count'];
|
||||
$filterData['rating'] = $data['rating'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['total_spent_type'])
|
||||
|| !isset($data['total_spent_amount']) || $data['total_spent_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_TOTAL_SPENT_FIELDS);
|
||||
}
|
||||
$filterData['total_spent_type'] = $data['total_spent_type'];
|
||||
$filterData['total_spent_amount'] = $data['total_spent_amount'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['single_order_value_type'])
|
||||
|| !isset($data['single_order_value_amount']) || $data['single_order_value_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS);
|
||||
}
|
||||
$filterData['single_order_value_type'] = $data['single_order_value_type'];
|
||||
$filterData['single_order_value_amount'] = $data['single_order_value_amount'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif (in_array($data['action'], [WooCommercePurchaseDate::ACTION, WooCommerceFirstOrder::ACTION])) {
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['value'] = $data['value'];
|
||||
} elseif ($data['action'] === WooCommerceAverageSpent::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['average_spent_type'])
|
||||
|| !isset($data['average_spent_amount']) || $data['average_spent_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_AVERAGE_SPENT_FIELDS);
|
||||
}
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['average_spent_amount'] = $data['average_spent_amount'];
|
||||
$filterData['average_spent_type'] = $data['average_spent_type'];
|
||||
} elseif ($data['action'] === WooCommerceUsedPaymentMethod::ACTION) {
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedPaymentMethod::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!isset($data['payment_methods']) || !is_array($data['payment_methods']) || empty($data['payment_methods'])) {
|
||||
throw new InvalidFilterException('Missing payment gateways', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['payment_methods'] = $data['payment_methods'];
|
||||
$filterData['days'] = intval($data['days'] ?? 0);
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceUsedShippingMethod::ACTION) {
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedShippingMethod::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!isset($data['shipping_methods']) || !is_array($data['shipping_methods']) || empty($data['shipping_methods'])) {
|
||||
throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['shipping_methods'] = $data['shipping_methods'];
|
||||
$filterData['days'] = intval($data['days'] ?? 0);
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif (in_array($data['action'], WooCommerceCustomerTextField::ACTIONS)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['value'] = $data['value'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['action'] = $data['action'];
|
||||
} elseif ($data['action'] === WooCommerceUsedCouponCode::ACTION) {
|
||||
$this->wooCommerceUsedCouponCode->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['coupon_code_ids'] = $data['coupon_code_ids'];
|
||||
$filterData['days'] = $data['days'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommercePurchasedWithAttribute::ACTION) {
|
||||
$this->wooCommercePurchasedWithAttribute->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['attribute_taxonomy_slug'] = $data['attribute_taxonomy_slug'] ?? null;
|
||||
$filterData['attribute_term_ids'] = $data['attribute_term_ids'] ?? null;
|
||||
$filterData['attribute_type'] = $data['attribute_type'];
|
||||
$filterData['attribute_local_name'] = $data['attribute_local_name'] ?? null;
|
||||
$filterData['attribute_local_values'] = $data['attribute_local_values'] ?? null;
|
||||
} elseif ($data['action'] === WooCommerceTag::ACTION) {
|
||||
$this->wooCommerceTag->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['tag_ids'] = $data['tag_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerceMembership(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceMembership::ACTION_MEMBER_OF) {
|
||||
if (!isset($data['plan_ids']) || !is_array($data['plan_ids'])) {
|
||||
throw new InvalidFilterException('Missing plan', InvalidFilterException::MISSING_PLAN_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['plan_ids'] = $data['plan_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerceSubscription(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceSubscription::ACTION_HAS_ACTIVE) {
|
||||
if (!isset($data['product_ids']) || !is_array($data['product_ids'])) {
|
||||
throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['product_ids'] = $data['product_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\AutomationsEvents;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailsReceived;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\Filter;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\NumberOfClicks;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedViaForm;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\UserRole;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceFirstOrder;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedCouponCode;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedShippingMethod;
|
||||
|
||||
class FilterFactory {
|
||||
/** @var EmailAction */
|
||||
private $emailAction;
|
||||
|
||||
/** @var UserRole */
|
||||
private $userRole;
|
||||
|
||||
/** @var WooCommerceAverageSpent */
|
||||
private $wooCommerceAverageSpent;
|
||||
|
||||
/** @var WooCommerceProduct */
|
||||
private $wooCommerceProduct;
|
||||
|
||||
/** @var WooCommerceCategory */
|
||||
private $wooCommerceCategory;
|
||||
|
||||
/** @var WooCommerceCountry */
|
||||
private $wooCommerceCountry;
|
||||
|
||||
/** @var WooCommerceNumberOfOrders */
|
||||
private $wooCommerceNumberOfOrders;
|
||||
|
||||
/** @var WooCommercePurchaseDate */
|
||||
private $wooCommercePurchaseDate;
|
||||
|
||||
/** @var WooCommerceSingleOrderValue */
|
||||
private $wooCommerceSingleOrderValue;
|
||||
|
||||
/** @var WooCommerceTotalSpent */
|
||||
private $wooCommerceTotalSpent;
|
||||
|
||||
/** @var WooCommerceMembership */
|
||||
private $wooCommerceMembership;
|
||||
|
||||
/** @var WooCommerceSubscription */
|
||||
private $wooCommerceSubscription;
|
||||
|
||||
/** @var EmailOpensAbsoluteCountAction */
|
||||
private $emailOpensAbsoluteCount;
|
||||
|
||||
/** @var SubscriberScore */
|
||||
private $subscriberScore;
|
||||
|
||||
/** @var MailPoetCustomFields */
|
||||
private $mailPoetCustomFields;
|
||||
|
||||
/** @var SubscriberSegment */
|
||||
private $subscriberSegment;
|
||||
|
||||
/** @var SubscriberTag */
|
||||
private $subscriberTag;
|
||||
|
||||
/** @var EmailActionClickAny */
|
||||
private $emailActionClickAny;
|
||||
|
||||
/** @var SubscriberSubscribedViaForm */
|
||||
private $subscribedViaForm;
|
||||
|
||||
/** @var SubscriberTextField */
|
||||
private $subscriberTextField;
|
||||
|
||||
/** @var WooCommerceUsedPaymentMethod */
|
||||
private $wooCommerceUsedPaymentMethod;
|
||||
|
||||
/** @var WooCommerceUsedShippingMethod */
|
||||
private $wooCommerceUsedShippingMethod;
|
||||
|
||||
/** @var WooCommerceCustomerTextField */
|
||||
private $wooCommerceCustomerTextField;
|
||||
|
||||
/** @var SubscriberDateField */
|
||||
private $subscriberDateField;
|
||||
|
||||
/** @var AutomationsEvents */
|
||||
private $automationsEvents;
|
||||
|
||||
/** @var WooCommerceNumberOfReviews */
|
||||
private $wooCommerceNumberOfReviews;
|
||||
|
||||
/** @var WooCommerceUsedCouponCode */
|
||||
private $wooCommerceUsedCouponCode;
|
||||
|
||||
/** @var WooCommerceFirstOrder */
|
||||
private $wooCommerceFirstOrder;
|
||||
|
||||
/** @var EmailsReceived */
|
||||
private $emailsReceived;
|
||||
|
||||
/** @var NumberOfClicks */
|
||||
private $numberOfClicks;
|
||||
|
||||
private WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute;
|
||||
|
||||
private WooCommerceTag $wooCommerceTag;
|
||||
|
||||
public function __construct(
|
||||
EmailAction $emailAction,
|
||||
EmailActionClickAny $emailActionClickAny,
|
||||
UserRole $userRole,
|
||||
MailPoetCustomFields $mailPoetCustomFields,
|
||||
WooCommerceProduct $wooCommerceProduct,
|
||||
WooCommerceCategory $wooCommerceCategory,
|
||||
WooCommerceCountry $wooCommerceCountry,
|
||||
WooCommerceCustomerTextField $wooCommerceCustomerTextField,
|
||||
EmailOpensAbsoluteCountAction $emailOpensAbsoluteCount,
|
||||
WooCommerceNumberOfOrders $wooCommerceNumberOfOrders,
|
||||
WooCommerceNumberOfReviews $wooCommerceNumberOfReviews,
|
||||
WooCommerceTotalSpent $wooCommerceTotalSpent,
|
||||
WooCommerceMembership $wooCommerceMembership,
|
||||
WooCommerceFirstOrder $wooCommerceFirstOrder,
|
||||
WooCommercePurchaseDate $wooCommercePurchaseDate,
|
||||
WooCommerceSubscription $wooCommerceSubscription,
|
||||
SubscriberScore $subscriberScore,
|
||||
SubscriberTag $subscriberTag,
|
||||
SubscriberSegment $subscriberSegment,
|
||||
SubscriberSubscribedViaForm $subscribedViaForm,
|
||||
WooCommerceSingleOrderValue $wooCommerceSingleOrderValue,
|
||||
WooCommerceAverageSpent $wooCommerceAverageSpent,
|
||||
WooCommerceTag $wooCommerceTag,
|
||||
WooCommerceUsedCouponCode $wooCommerceUsedCouponCode,
|
||||
WooCommerceUsedPaymentMethod $wooCommerceUsedPaymentMethod,
|
||||
WooCommerceUsedShippingMethod $wooCommerceUsedShippingMethod,
|
||||
SubscriberTextField $subscriberTextField,
|
||||
SubscriberDateField $subscriberDateField,
|
||||
AutomationsEvents $automationsEvents,
|
||||
EmailsReceived $emailsReceived,
|
||||
NumberOfClicks $numberOfClicks,
|
||||
WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute
|
||||
) {
|
||||
$this->emailAction = $emailAction;
|
||||
$this->userRole = $userRole;
|
||||
$this->wooCommerceProduct = $wooCommerceProduct;
|
||||
$this->wooCommerceCategory = $wooCommerceCategory;
|
||||
$this->wooCommerceCountry = $wooCommerceCountry;
|
||||
$this->wooCommerceNumberOfOrders = $wooCommerceNumberOfOrders;
|
||||
$this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews;
|
||||
$this->wooCommerceMembership = $wooCommerceMembership;
|
||||
$this->wooCommercePurchaseDate = $wooCommercePurchaseDate;
|
||||
$this->wooCommerceSubscription = $wooCommerceSubscription;
|
||||
$this->emailOpensAbsoluteCount = $emailOpensAbsoluteCount;
|
||||
$this->wooCommerceTotalSpent = $wooCommerceTotalSpent;
|
||||
$this->subscriberScore = $subscriberScore;
|
||||
$this->subscriberTag = $subscriberTag;
|
||||
$this->mailPoetCustomFields = $mailPoetCustomFields;
|
||||
$this->subscriberSegment = $subscriberSegment;
|
||||
$this->emailActionClickAny = $emailActionClickAny;
|
||||
$this->wooCommerceSingleOrderValue = $wooCommerceSingleOrderValue;
|
||||
$this->subscriberTextField = $subscriberTextField;
|
||||
$this->subscribedViaForm = $subscribedViaForm;
|
||||
$this->wooCommerceAverageSpent = $wooCommerceAverageSpent;
|
||||
$this->wooCommerceUsedPaymentMethod = $wooCommerceUsedPaymentMethod;
|
||||
$this->wooCommerceUsedShippingMethod = $wooCommerceUsedShippingMethod;
|
||||
$this->wooCommerceCustomerTextField = $wooCommerceCustomerTextField;
|
||||
$this->automationsEvents = $automationsEvents;
|
||||
$this->subscriberDateField = $subscriberDateField;
|
||||
$this->wooCommerceUsedCouponCode = $wooCommerceUsedCouponCode;
|
||||
$this->wooCommerceFirstOrder = $wooCommerceFirstOrder;
|
||||
$this->emailsReceived = $emailsReceived;
|
||||
$this->numberOfClicks = $numberOfClicks;
|
||||
$this->wooCommercePurchasedWithAttribute = $wooCommercePurchasedWithAttribute;
|
||||
$this->wooCommerceTag = $wooCommerceTag;
|
||||
}
|
||||
|
||||
public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter {
|
||||
$filterData = $filter->getFilterData();
|
||||
$filterType = $filterData->getFilterType();
|
||||
$action = $filterData->getAction();
|
||||
switch ($filterType) {
|
||||
case DynamicSegmentFilterData::TYPE_AUTOMATIONS:
|
||||
return $this->automationsEvents;
|
||||
case DynamicSegmentFilterData::TYPE_USER_ROLE:
|
||||
return $this->userRole($action);
|
||||
case DynamicSegmentFilterData::TYPE_EMAIL:
|
||||
return $this->email($action);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP:
|
||||
return $this->wooCommerceMembership();
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION:
|
||||
return $this->wooCommerceSubscription();
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE:
|
||||
return $this->wooCommerce($action);
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
*
|
||||
* @return MailPoetCustomFields|SubscriberScore|SubscriberSegment|UserRole|SubscriberTag|SubscriberTextField|SubscriberSubscribedViaForm|SubscriberDateField
|
||||
*/
|
||||
private function userRole(?string $action) {
|
||||
if ($action === SubscriberScore::TYPE) {
|
||||
return $this->subscriberScore;
|
||||
} elseif ($action === MailPoetCustomFields::TYPE) {
|
||||
return $this->mailPoetCustomFields;
|
||||
} elseif ($action === SubscriberSegment::TYPE) {
|
||||
return $this->subscriberSegment;
|
||||
} elseif ($action === SubscriberTag::TYPE) {
|
||||
return $this->subscriberTag;
|
||||
} elseif ($action === SubscriberSubscribedViaForm::TYPE) {
|
||||
return $this->subscribedViaForm;
|
||||
} elseif (in_array($action, SubscriberTextField::TYPES)) {
|
||||
return $this->subscriberTextField;
|
||||
} elseif (in_array($action, SubscriberDateField::TYPES)) {
|
||||
return $this->subscriberDateField;
|
||||
}
|
||||
return $this->userRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
* @return EmailAction|EmailActionClickAny|EmailOpensAbsoluteCountAction|EmailsReceived|NumberOfClicks
|
||||
*/
|
||||
private function email(?string $action) {
|
||||
$countActions = [EmailOpensAbsoluteCountAction::TYPE, EmailOpensAbsoluteCountAction::MACHINE_TYPE];
|
||||
if (in_array($action, $countActions)) {
|
||||
return $this->emailOpensAbsoluteCount;
|
||||
} elseif ($action === EmailActionClickAny::TYPE) {
|
||||
return $this->emailActionClickAny;
|
||||
} elseif ($action === EmailsReceived::ACTION) {
|
||||
return $this->emailsReceived;
|
||||
} elseif ($action === NumberOfClicks::ACTION) {
|
||||
return $this->numberOfClicks;
|
||||
}
|
||||
return $this->emailAction;
|
||||
}
|
||||
|
||||
private function wooCommerceMembership(): WooCommerceMembership {
|
||||
return $this->wooCommerceMembership;
|
||||
}
|
||||
|
||||
private function wooCommerceSubscription(): WooCommerceSubscription {
|
||||
return $this->wooCommerceSubscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
* @return Filter
|
||||
*/
|
||||
private function wooCommerce(?string $action) {
|
||||
if ($action === WooCommerceProduct::ACTION_PRODUCT) {
|
||||
return $this->wooCommerceProduct;
|
||||
} elseif (in_array($action, WooCommerceNumberOfOrders::ACTIONS)) {
|
||||
return $this->wooCommerceNumberOfOrders;
|
||||
} elseif ($action === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) {
|
||||
return $this->wooCommerceTotalSpent;
|
||||
} elseif ($action === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) {
|
||||
return $this->wooCommerceCountry;
|
||||
} elseif ($action === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) {
|
||||
return $this->wooCommerceSingleOrderValue;
|
||||
} elseif ($action === WooCommercePurchaseDate::ACTION) {
|
||||
return $this->wooCommercePurchaseDate;
|
||||
} elseif ($action === WooCommerceAverageSpent::ACTION) {
|
||||
return $this->wooCommerceAverageSpent;
|
||||
} elseif ($action === WooCommerceUsedPaymentMethod::ACTION) {
|
||||
return $this->wooCommerceUsedPaymentMethod;
|
||||
} elseif ($action === WooCommerceUsedShippingMethod::ACTION) {
|
||||
return $this->wooCommerceUsedShippingMethod;
|
||||
} elseif ($action === WooCommerceNumberOfReviews::ACTION) {
|
||||
return $this->wooCommerceNumberOfReviews;
|
||||
} elseif (in_array($action, WooCommerceCustomerTextField::ACTIONS)) {
|
||||
return $this->wooCommerceCustomerTextField;
|
||||
} elseif ($action === WooCommerceUsedCouponCode::ACTION) {
|
||||
return $this->wooCommerceUsedCouponCode;
|
||||
} elseif ($action === WooCommerceFirstOrder::ACTION) {
|
||||
return $this->wooCommerceFirstOrder;
|
||||
} elseif ($action === WooCommercePurchasedWithAttribute::ACTION) {
|
||||
return $this->wooCommercePurchasedWithAttribute;
|
||||
} elseif ($action == WooCommerceTag::ACTION) {
|
||||
return $this->wooCommerceTag;
|
||||
}
|
||||
return $this->wooCommerceCategory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\SegmentDependencyValidator;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class FilterHandler {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var SegmentDependencyValidator */
|
||||
private $segmentDependencyValidator;
|
||||
|
||||
/** @var FilterFactory */
|
||||
private $filterFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
SegmentDependencyValidator $segmentDependencyValidator,
|
||||
FilterFactory $filterFactory
|
||||
) {
|
||||
|
||||
$this->entityManager = $entityManager;
|
||||
$this->segmentDependencyValidator = $segmentDependencyValidator;
|
||||
$this->filterFactory = $filterFactory;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, SegmentEntity $segment): QueryBuilder {
|
||||
$filters = $segment->getDynamicFilters();
|
||||
$filterSelects = [];
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$pluginsForAllFiltersMissing = $this->segmentDependencyValidator->getMissingPluginsByAllFilters($filters);
|
||||
foreach ($filters as $filter) {
|
||||
$subscribersIdsQuery = $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select("DISTINCT $subscribersTable.id as inner_subscriber_id")
|
||||
->from($subscribersTable);
|
||||
// When a required plugin is missing we want to return empty result
|
||||
if ($pluginsForAllFiltersMissing || $this->segmentDependencyValidator->getMissingPluginsByFilter($filter)) {
|
||||
$subscribersIdsQuery->andWhere('1 = 0');
|
||||
} else {
|
||||
$this->filterFactory->getFilterForFilterEntity($filter)->apply($subscribersIdsQuery, $filter);
|
||||
}
|
||||
$filterSelects[] = $subscribersIdsQuery->getSQL();
|
||||
$queryBuilder->setParameters(
|
||||
array_merge(
|
||||
$subscribersIdsQuery->getParameters(),
|
||||
$queryBuilder->getParameters()
|
||||
),
|
||||
array_merge(
|
||||
$subscribersIdsQuery->getParameterTypes(),
|
||||
$queryBuilder->getParameterTypes()
|
||||
)
|
||||
);
|
||||
}
|
||||
$this->joinSubqueries($queryBuilder, $segment, $filterSelects);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function joinSubqueries(QueryBuilder $queryBuilder, SegmentEntity $segment, array $subQueries): QueryBuilder {
|
||||
$filter = $segment->getDynamicFilters()->first();
|
||||
if (!$filter) return $queryBuilder;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
if ($segment->getFiltersConnectOperator() === DynamicSegmentFilterData::CONNECT_TYPE_OR) {
|
||||
// the final query: SELECT * FROM subscribers INNER JOIN (filter_select1 UNION filter_select2) filtered_subscribers ON filtered_subscribers.inner_subscriber_id = id
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf('(%s)', join(' UNION ', $subQueries)),
|
||||
'filtered_subscribers',
|
||||
"filtered_subscribers.inner_subscriber_id = $subscribersTable.id"
|
||||
);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
foreach ($subQueries as $key => $subQuery) {
|
||||
// we need a unique name for each subquery so that we can join them together in the sql query - just make sure the identifier starts with a letter, not a number
|
||||
$subqueryName = 'a' . $key;
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
"($subQuery)",
|
||||
$subqueryName,
|
||||
"$subqueryName.inner_subscriber_id = $subscribersTable.id"
|
||||
);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class AutomationsEvents implements Filter {
|
||||
|
||||
const SUPPORTED_ACTIONS = [
|
||||
self::ENTERED_ACTION,
|
||||
self::EXITED_ACTION,
|
||||
];
|
||||
|
||||
const ENTERED_ACTION = 'enteredAutomation';
|
||||
const EXITED_ACTION = 'exitedAutomation';
|
||||
const AUTOMATION_IDS_PARAM = 'automation_ids';
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$automationIds = $filterData->getParam(self::AUTOMATION_IDS_PARAM);
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $action, $automationIds);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $action, $automationIds);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $action, $automationIds);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, $action, $automationIds) {
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$automationsTable = $this->filterHelper->getPrefixedTable('mailpoet_automations');
|
||||
$automationRunsTable = $this->filterHelper->getPrefixedTable('mailpoet_automation_runs');
|
||||
$automationRunSubjectsTable = $this->filterHelper->getPrefixedTable('mailpoet_automation_run_subjects');
|
||||
$automationIdsParam = $this->filterHelper->getUniqueParameterName('automationIds');
|
||||
|
||||
$queryBuilder
|
||||
->innerJoin(
|
||||
$subscribersTable,
|
||||
$automationRunSubjectsTable,
|
||||
'subjects',
|
||||
"subjects.key = 'mailpoet:subscriber' AND subjects.args = CONCAT('{\"subscriber_id\":', $subscribersTable.id, '}')"
|
||||
)
|
||||
->innerJoin(
|
||||
'subjects',
|
||||
$automationRunsTable,
|
||||
'runs',
|
||||
'subjects.automation_run_id = runs.id'
|
||||
)
|
||||
->innerJoin(
|
||||
'runs',
|
||||
$automationsTable,
|
||||
'automations',
|
||||
'automations.id = runs.automation_id'
|
||||
)
|
||||
->andWhere("automations.id IN (:$automationIdsParam)")
|
||||
->setParameter($automationIdsParam, $automationIds, ArrayParameterType::STRING);
|
||||
|
||||
if ($action === self::EXITED_ACTION) {
|
||||
$statusParam = $this->filterHelper->getUniqueParameterName('status');
|
||||
$queryBuilder
|
||||
->andWhere("runs.status != :$statusParam")
|
||||
->setParameter($statusParam, AutomationRun::STATUS_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, $action, $automationIds) {
|
||||
$this->applyForAnyOperator($queryBuilder, $action, $automationIds);
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT automations.id) = " . count($automationIds));
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$automationIds = $filterData->getArrayParam(self::AUTOMATION_IDS_PARAM);
|
||||
$lookupData = [
|
||||
'automations' => [],
|
||||
];
|
||||
|
||||
foreach ($automationIds as $automationId) {
|
||||
$automation = $this->automationStorage->getAutomation(intval($automationId));
|
||||
if ($automation instanceof Automation) {
|
||||
$lookupData['automations'][$automationId] = $automation->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
|
||||
class DateFilterHelper {
|
||||
const BEFORE = 'before';
|
||||
const AFTER = 'after';
|
||||
const ON = 'on';
|
||||
const ON_OR_BEFORE = 'onOrBefore';
|
||||
const ON_OR_AFTER = 'onOrAfter';
|
||||
const NOT_ON = 'notOn';
|
||||
const IN_THE_LAST = 'inTheLast';
|
||||
const NOT_IN_THE_LAST = 'notInTheLast';
|
||||
|
||||
public function getValidOperators(): array {
|
||||
return array_merge(
|
||||
$this->getAbsoluteDateOperators(),
|
||||
$this->getRelativeDateOperators()
|
||||
);
|
||||
}
|
||||
|
||||
public function getAbsoluteDateOperators(): array {
|
||||
return [
|
||||
self::BEFORE,
|
||||
self::AFTER,
|
||||
self::ON,
|
||||
self::ON_OR_BEFORE,
|
||||
self::ON_OR_AFTER,
|
||||
self::NOT_ON,
|
||||
];
|
||||
}
|
||||
|
||||
public function getRelativeDateOperators(): array {
|
||||
return [
|
||||
self::IN_THE_LAST,
|
||||
self::NOT_IN_THE_LAST,
|
||||
];
|
||||
}
|
||||
|
||||
public function getDateStringForOperator(string $operator, string $value): string {
|
||||
if (in_array($operator, self::getAbsoluteDateOperators())) {
|
||||
$carbon = CarbonImmutable::createFromFormat('Y-m-d', $value);
|
||||
if (!$carbon instanceof CarbonImmutable) {
|
||||
throw new InvalidFilterException('Invalid date value', InvalidFilterException::INVALID_DATE_VALUE);
|
||||
}
|
||||
} else if (in_array($operator, self::getRelativeDateOperators())) {
|
||||
$carbon = CarbonImmutable::now()->subDays(intval($value) - 1);
|
||||
} else {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
return $carbon->toDateString();
|
||||
}
|
||||
|
||||
public function getDateValueFromFilter(DynamicSegmentFilterEntity $filter): string {
|
||||
$filterData = $filter->getFilterData();
|
||||
$dateValue = $filterData->getParam('value');
|
||||
if (!is_string($dateValue)) {
|
||||
throw new InvalidFilterException('Incorrect value for date', InvalidFilterException::INVALID_DATE_VALUE);
|
||||
}
|
||||
return $dateValue;
|
||||
}
|
||||
|
||||
public function getOperatorFromFilter(DynamicSegmentFilterEntity $filter): string {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!is_string($operator) || !in_array($operator, $this->getValidOperators())) {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return $operator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\StatisticsNewsletterEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Newsletter\NewslettersRepository;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailAction implements Filter {
|
||||
const ACTION_OPENED = 'opened';
|
||||
const ACTION_MACHINE_OPENED = 'machineOpened';
|
||||
/** @deprecated */
|
||||
const ACTION_NOT_OPENED = 'notOpened';
|
||||
const ACTION_CLICKED = 'clicked';
|
||||
const ACTION_WAS_SENT = 'wasSent';
|
||||
/** @deprecated */
|
||||
const ACTION_NOT_CLICKED = 'notClicked';
|
||||
|
||||
const ALLOWED_ACTIONS = [
|
||||
self::ACTION_OPENED,
|
||||
self::ACTION_MACHINE_OPENED,
|
||||
self::ACTION_CLICKED,
|
||||
self::ACTION_WAS_SENT,
|
||||
EmailActionClickAny::TYPE,
|
||||
EmailOpensAbsoluteCountAction::TYPE,
|
||||
EmailOpensAbsoluteCountAction::MACHINE_TYPE,
|
||||
EmailsReceived::ACTION,
|
||||
NumberOfClicks::ACTION,
|
||||
];
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var NewslettersRepository */
|
||||
private $newslettersRepository;
|
||||
|
||||
/** @var NewsletterLinkRepository */
|
||||
private $newsletterLinkRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
NewslettersRepository $newslettersRepository,
|
||||
NewsletterLinkRepository $newsletterLinkRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->newslettersRepository = $newslettersRepository;
|
||||
$this->newsletterLinkRepository = $newsletterLinkRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getAction();
|
||||
$parameterSuffix = (string)($filter->getId() ?? Security::generateRandomString());
|
||||
|
||||
if ($action === self::ACTION_CLICKED) {
|
||||
return $this->applyForClickedActions($queryBuilder, $filterData, $parameterSuffix);
|
||||
} elseif ($action === self::ACTION_WAS_SENT) {
|
||||
return $this->applyForWasSentAction($queryBuilder, $filterData, $parameterSuffix);
|
||||
} else {
|
||||
return $this->applyForOpenedActions($queryBuilder, $filterData, $parameterSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForClickedActions(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$action = $filterData->getAction();
|
||||
$newsletterId = $filterData->getParam('newsletter_id');
|
||||
$linkIds = $filterData->getParam('link_ids');
|
||||
if (!is_array($linkIds)) {
|
||||
$linkIds = [];
|
||||
}
|
||||
|
||||
$statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
|
||||
$where = '1';
|
||||
|
||||
if (($action === self::ACTION_NOT_CLICKED) || ($operator === DynamicSegmentFilterData::OPERATOR_NONE)) {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsSentTable,
|
||||
'statssent',
|
||||
"$subscribersTable.id = statssent.subscriber_id AND statssent.newsletter_id = :newsletter" . $parameterSuffix
|
||||
)->leftJoin(
|
||||
'statssent',
|
||||
$statsTable,
|
||||
'stats',
|
||||
$this->createNotStatsJoinCondition($parameterSuffix, $linkIds)
|
||||
)->setParameter('newsletter' . $parameterSuffix, $newsletterId);
|
||||
$where .= ' AND stats.id IS NULL';
|
||||
} else {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id AND stats.newsletter_id = :newsletter" . $parameterSuffix
|
||||
)->setParameter('newsletter' . $parameterSuffix, $newsletterId);
|
||||
}
|
||||
|
||||
if ($action === EmailAction::ACTION_CLICKED && $operator !== DynamicSegmentFilterData::OPERATOR_NONE && $linkIds) {
|
||||
$where .= ' AND stats.link_id IN (:links' . $parameterSuffix . ')';
|
||||
}
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
if ($linkIds) {
|
||||
$queryBuilder->having('COUNT(1) = ' . count($linkIds));
|
||||
} else {
|
||||
// Case when a user selects all of, but doesn't specify links == all of all links.
|
||||
$linksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
$linksQueryBuilder = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$linkCount = $linksQueryBuilder->select('count(id)')
|
||||
->from($linksTable)
|
||||
->where('newsletter_id = :newsletter_id')
|
||||
->setParameter('newsletter_id', $newsletterId)
|
||||
->execute()
|
||||
->fetchOne();
|
||||
$queryBuilder->having('COUNT(1) = ' . $linkCount);
|
||||
}
|
||||
}
|
||||
$queryBuilder = $queryBuilder->andWhere($where);
|
||||
if ($linkIds) {
|
||||
$queryBuilder = $queryBuilder
|
||||
->setParameter('links' . $parameterSuffix, $linkIds, ArrayParameterType::STRING);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForOpenedActions(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$action = $filterData->getAction();
|
||||
$newsletters = $filterData->getParam('newsletters');
|
||||
|
||||
$statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
|
||||
|
||||
$where = '1';
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsSentTable,
|
||||
'statssent',
|
||||
"$subscribersTable.id = statssent.subscriber_id AND statssent.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->leftJoin(
|
||||
'statssent',
|
||||
$statsTable,
|
||||
'stats',
|
||||
"statssent.subscriber_id = stats.subscriber_id AND stats.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
$where .= ' AND stats.id IS NULL';
|
||||
} else {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id AND stats.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($newsletters));
|
||||
}
|
||||
}
|
||||
if (($action === EmailAction::ACTION_OPENED) && ($operator !== DynamicSegmentFilterData::OPERATOR_NONE)) {
|
||||
$queryBuilder->andWhere('stats.user_agent_type = :userAgentType')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
|
||||
}
|
||||
if ($action === EmailAction::ACTION_MACHINE_OPENED) {
|
||||
$queryBuilder->andWhere('(stats.user_agent_type = :userAgentType)')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE);
|
||||
}
|
||||
$queryBuilder = $queryBuilder->andWhere($where);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function createNotStatsJoinCondition(string $parameterSuffix, array $linkIds = null): string {
|
||||
$clause = "statssent.subscriber_id = stats.subscriber_id AND stats.newsletter_id = :newsletter" . $parameterSuffix;
|
||||
if ($linkIds) {
|
||||
$clause .= ' AND stats.link_id IN (:links' . $parameterSuffix . ')';
|
||||
}
|
||||
return $clause;
|
||||
}
|
||||
|
||||
private function applyForWasSentAction(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$newsletters = (array)$filterData->getParam('newsletters');
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$statisticsNewslettersTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->leftJoin(
|
||||
$this->filterHelper->getSubscribersTable(),
|
||||
$statisticsNewslettersTable,
|
||||
'statisticsNewsletter',
|
||||
"$subscribersTable.id = statisticsNewsletter.subscriber_id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)
|
||||
->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER)
|
||||
->andWhere('statisticsNewsletter.subscriber_id IS NULL');
|
||||
} else {
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statisticsNewslettersTable,
|
||||
'statisticsNewsletter',
|
||||
"statisticsNewsletter.subscriber_id = $subscribersTable.id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($newsletters));
|
||||
}
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'newsletters' => [],
|
||||
'links' => [],
|
||||
];
|
||||
$newsletterIds = $filterData->getParam('newsletters');
|
||||
if (!is_array($newsletterIds)) {
|
||||
$newsletterIds = [];
|
||||
}
|
||||
|
||||
// Clicked action only supports single newsletter ID
|
||||
$singularNewsletterId = $filterData->getParam('newsletter_id');
|
||||
if (!is_null($singularNewsletterId)) {
|
||||
$newsletterIds[] = $singularNewsletterId;
|
||||
}
|
||||
|
||||
$linkIds = $filterData->getParam('link_ids');
|
||||
if (!is_array($linkIds)) {
|
||||
$linkIds = [];
|
||||
}
|
||||
|
||||
foreach ($newsletterIds as $newsletterId) {
|
||||
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
|
||||
if ($newsletter instanceof NewsletterEntity) {
|
||||
$lookupData['newsletters'][$newsletterId] = $newsletter->getSubject();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($linkIds as $linkId) {
|
||||
$link = $this->newsletterLinkRepository->findOneById($linkId);
|
||||
if ($link instanceof NewsletterLinkEntity) {
|
||||
$lookupData['links'][$linkId] = $link->getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailActionClickAny implements Filter {
|
||||
const TYPE = 'clickedAny';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$newsletterLinksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
|
||||
$excludedLinks = [
|
||||
'[link:subscription_unsubscribe_url]',
|
||||
'[link:subscription_instant_unsubscribe_url]',
|
||||
'[link:newsletter_view_in_browser_url]',
|
||||
'[link:subscription_manage_url]',
|
||||
];
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id"
|
||||
)->innerJoin(
|
||||
'stats',
|
||||
$newsletterLinksTable,
|
||||
'newsletterLinks',
|
||||
"stats.link_id = newsletterLinks.id AND newsletterLinks.URL NOT IN ('" . join("', '", $excludedLinks) . "')"
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailOpensAbsoluteCountAction implements Filter {
|
||||
const TYPE = 'opensAbsoluteCount';
|
||||
const MACHINE_TYPE = 'machineOpensAbsoluteCount';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$days = $filterData->getParam('days');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$action = $filterData->getAction();
|
||||
$timeframe = $filterData->getParam('timeframe');
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'opens',
|
||||
"{$subscribersTable}.id = opens.subscriber_id AND opens.user_agent_type = :userAgentType{$parameterSuffix}"
|
||||
);
|
||||
} else {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'opens',
|
||||
"{$subscribersTable}.id = opens.subscriber_id AND opens.created_at > :newer{$parameterSuffix} AND opens.user_agent_type = :userAgentType{$parameterSuffix}"
|
||||
);
|
||||
$queryBuilder->setParameter('newer' . $parameterSuffix, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(opens.id) = :opens" . $parameterSuffix);
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(opens.id) != :opens" . $parameterSuffix);
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(opens.id) < :opens" . $parameterSuffix);
|
||||
} else {
|
||||
$queryBuilder->having("count(opens.id) > :opens" . $parameterSuffix);
|
||||
}
|
||||
$queryBuilder->setParameter('opens' . $parameterSuffix, $filterData->getParam('opens'));
|
||||
|
||||
if ($action === EmailOpensAbsoluteCountAction::TYPE) {
|
||||
$queryBuilder->setParameter('userAgentType' . $parameterSuffix, UserAgentEntity::USER_AGENT_TYPE_HUMAN);
|
||||
} else {
|
||||
$queryBuilder->setParameter('userAgentType' . $parameterSuffix, UserAgentEntity::USER_AGENT_TYPE_MACHINE);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsNewsletterEntity;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailsReceived implements Filter {
|
||||
const ACTION = 'numberReceived';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$emailCount = $filterData->getIntParam('emails');
|
||||
$operator = $filterData->getStringParam('operator');
|
||||
$timeframe = $filterData->getStringParam('timeframe');
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'emails', "{$subscribersTable}.id = emails.subscriber_id");
|
||||
} else {
|
||||
$days = $filterData->getIntParam('days');
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('days');
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'emails', "{$subscribersTable}.id = emails.subscriber_id AND emails.sent_at >= :$dateParam");
|
||||
$queryBuilder->setParameter($dateParam, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
$emailCountParam = $this->filterHelper->getUniqueParameterName('emails');
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(emails.id) = :$emailCountParam");
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(emails.id) != :$emailCountParam");
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(emails.id) < :$emailCountParam");
|
||||
} else {
|
||||
$queryBuilder->having("count(emails.id) > :$emailCountParam");
|
||||
}
|
||||
$queryBuilder->setParameter($emailCountParam, $emailCount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
interface Filter {
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder;
|
||||
|
||||
/**
|
||||
* At sending time, we store the current state of every filter so we can tell in the future how it was configured. This
|
||||
* method should be used to return any data that might change after sending time. For example, if a filter stores IDs
|
||||
* of related entities, we should try to look up descriptive names for those entities in case they get deleted or
|
||||
* renamed later.
|
||||
*
|
||||
* @param DynamicSegmentFilterData $filterData
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class FilterHelper {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function getPrefixedTable(string $table): string {
|
||||
global $wpdb;
|
||||
return sprintf('%s%s', $wpdb->prefix, $table);
|
||||
}
|
||||
|
||||
public function getNewSubscribersQueryBuilder(): QueryBuilder {
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select($this->getSubscribersTable() . '.id')
|
||||
->from($this->getSubscribersTable());
|
||||
}
|
||||
|
||||
public function getSubscribersTable(): string {
|
||||
return $this->getTableForEntity(SubscriberEntity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<object> $entityClass
|
||||
*/
|
||||
public function getTableForEntity(string $entityClass): string {
|
||||
return $this->entityManager->getClassMetadata($entityClass)->getTableName();
|
||||
}
|
||||
|
||||
public function getInterpolatedSQL(QueryBuilder $query): string {
|
||||
$sql = $query->getSQL();
|
||||
$params = $query->getParameters();
|
||||
$search = array_map(function($key) {
|
||||
return ":$key";
|
||||
}, array_keys($params));
|
||||
$replace = array_map(function($value) use ($query) {
|
||||
if (is_array($value)) {
|
||||
$quotedValues = array_map(function($arrayValue) use ($query) {
|
||||
return $query->expr()->literal($arrayValue);
|
||||
}, $value);
|
||||
return implode(',', $quotedValues);
|
||||
}
|
||||
return $query->expr()->literal($value);
|
||||
}, array_values($params));
|
||||
return str_replace($search, $replace, $sql);
|
||||
}
|
||||
|
||||
public function getUniqueParameterName(string $parameter): string {
|
||||
$suffix = Security::generateRandomString();
|
||||
return sprintf("%s_%s", $parameter, $suffix);
|
||||
}
|
||||
|
||||
public function validateDaysPeriodData(array $data): void {
|
||||
if (!isset($data['timeframe']) || !in_array($data['timeframe'], [DynamicSegmentFilterData::TIMEFRAME_ALL_TIME, DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST], true)) {
|
||||
throw new InvalidFilterException('Missing timeframe type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if ($data['timeframe'] === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
$days = intval($data['days'] ?? null);
|
||||
|
||||
if ($days < 1) {
|
||||
throw new InvalidFilterException('Missing number of days', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\CustomFields\CustomFieldsRepository;
|
||||
use MailPoet\Entities\CustomFieldEntity;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberCustomFieldEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class MailPoetCustomFields implements Filter {
|
||||
const TYPE = 'mailpoetCustomField';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var CustomFieldsRepository */
|
||||
private $customFieldsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
CustomFieldsRepository $customFieldsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->customFieldsRepository = $customFieldsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$customFieldType = $filterData->getParam('custom_field_type');
|
||||
$customFieldId = $filterData->getParam('custom_field_id');
|
||||
$parameterSuffix = (string)($filter->getId() ?? Security::generateRandomString());
|
||||
$customFieldIdParam = 'customFieldId' . $parameterSuffix;
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscribersCustomFieldTable = $this->entityManager->getClassMetadata(SubscriberCustomFieldEntity::class)->getTableName();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$subscribersCustomFieldTable,
|
||||
'subscribers_custom_field',
|
||||
"$subscribersTable.id = subscribers_custom_field.subscriber_id AND subscribers_custom_field.custom_field_id = :$customFieldIdParam"
|
||||
);
|
||||
$queryBuilder->setParameter($customFieldIdParam, $customFieldId);
|
||||
|
||||
$valueParam = 'value' . $parameterSuffix;
|
||||
if (
|
||||
($customFieldType === CustomFieldEntity::TYPE_TEXT)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_TEXTAREA)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_RADIO)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_SELECT)
|
||||
) {
|
||||
$queryBuilder = $this->applyEquality($queryBuilder, $filter, $valueParam);
|
||||
}
|
||||
if ($customFieldType === CustomFieldEntity::TYPE_CHECKBOX) {
|
||||
$queryBuilder = $this->applyForCheckbox($queryBuilder, $filter);
|
||||
}
|
||||
if ($customFieldType === CustomFieldEntity::TYPE_DATE) {
|
||||
$queryBuilder = $this->applyForDate($queryBuilder, $filter, $valueParam);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDate(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter, string $valueParam): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$dateType = $filterData->getParam('date_type');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
if ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NULL');
|
||||
return $queryBuilder;
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NOT NULL');
|
||||
return $queryBuilder;
|
||||
} elseif ($dateType === 'month') {
|
||||
return $this->applyForDateMonth($queryBuilder, $valueParam);
|
||||
} elseif ($dateType === 'year') {
|
||||
return $this->applyForDateYear($queryBuilder, $operator, $valueParam);
|
||||
}
|
||||
return $this->applyForDateEqual($queryBuilder, $operator, $valueParam);
|
||||
}
|
||||
|
||||
private function applyForDateMonth(QueryBuilder $queryBuilder, string $valueParam): QueryBuilder {
|
||||
$queryBuilder->andWhere("month(subscribers_custom_field.value) = month(:$valueParam)");
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDateYear(QueryBuilder $queryBuilder, ?string $operator, string $valueParam): QueryBuilder {
|
||||
if ($operator === 'before') {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) < year(:$valueParam)");
|
||||
} elseif ($operator === 'after') {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) > year(:$valueParam)");
|
||||
} else {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) = year(:$valueParam)");
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDateEqual(QueryBuilder $queryBuilder, ?string $operator, string $valueParam): QueryBuilder {
|
||||
if ($operator === 'before') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value < :$valueParam");
|
||||
} elseif ($operator === 'after') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value > :$valueParam");
|
||||
} else {
|
||||
// we always save full date in the database: 2018-03-01 00:00:00
|
||||
// so this works even for year_month where we save the first day of the month
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value = :$valueParam");
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForCheckbox(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NULL');
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NOT NULL');
|
||||
} elseif ($value === '1') {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value = 1');
|
||||
} elseif ($value === '0') {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value <> 1');
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyEquality(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter, string $valueParam): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
|
||||
$operator = $filterData->getParam('operator');
|
||||
$value = $filterData->getParam('value');
|
||||
|
||||
$requiresValue = !in_array($operator, [DynamicSegmentFilterData::IS_BLANK, DynamicSegmentFilterData::IS_NOT_BLANK]);
|
||||
|
||||
if ($requiresValue && !is_string($value)) {
|
||||
throw new InvalidFilterException('Missing required value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
/** @var string $value - for PhpStan */
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value = :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'not_equals') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value != :$valueParam");
|
||||
$queryBuilder->orWhere('subscribers_custom_field.value IS NULL');
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'more_than') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value > :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'less_than') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value < :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value IS NULL OR subscribers_custom_field.value = ''");
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value IS NOT NULL AND subscribers_custom_field.value != ''");
|
||||
} elseif ($operator === 'not_contains') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value NOT LIKE :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, '%' . Helpers::escapeSearch($value) . '%');
|
||||
} else {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value LIKE :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, '%' . Helpers::escapeSearch($value) . '%');
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'customFields' => [],
|
||||
];
|
||||
$customFieldId = $filterData->getIntParam('custom_field_id');
|
||||
$customField = $this->customFieldsRepository->findOneById($customFieldId);
|
||||
if ($customField instanceof CustomFieldEntity) {
|
||||
$lookupData['customFields'][$customFieldId] = $customField->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class NumberOfClicks implements Filter {
|
||||
const ACTION = 'numberOfClicks';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$clickCount = $filterData->getIntParam('clicks');
|
||||
$operator = $filterData->getStringParam('operator');
|
||||
$timeframe = $filterData->getStringParam('timeframe');
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'clicks', "{$subscribersTable}.id = clicks.subscriber_id");
|
||||
} else {
|
||||
$days = $filterData->getIntParam('days');
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('days');
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'clicks', "{$subscribersTable}.id = clicks.subscriber_id AND clicks.created_at >= :$dateParam");
|
||||
$queryBuilder->setParameter($dateParam, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
$clicksCountParam = $this->filterHelper->getUniqueParameterName('clicks');
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(clicks.id) = :$clicksCountParam");
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(clicks.id) != :$clicksCountParam");
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(clicks.id) < :$clicksCountParam");
|
||||
} else {
|
||||
$queryBuilder->having("count(clicks.id) > :$clicksCountParam");
|
||||
}
|
||||
$queryBuilder->setParameter($clicksCountParam, $clickCount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberDateField implements Filter {
|
||||
const LAST_CLICK_DATE = 'lastClickDate';
|
||||
const LAST_ENGAGEMENT_DATE = 'lastEngagementDate';
|
||||
const LAST_PURCHASE_DATE = 'lastPurchaseDate';
|
||||
const LAST_OPEN_DATE = 'lastOpenDate';
|
||||
const LAST_PAGE_VIEW_DATE = 'lastPageViewDate';
|
||||
const LAST_SENDING_DATE = 'lastSendingDate';
|
||||
|
||||
// Slightly different naming due to backwards compatibility
|
||||
const SUBSCRIBED_DATE = 'subscribedDate';
|
||||
|
||||
const TYPES = [
|
||||
self::LAST_CLICK_DATE,
|
||||
self::LAST_ENGAGEMENT_DATE,
|
||||
self::LAST_PURCHASE_DATE,
|
||||
self::LAST_OPEN_DATE,
|
||||
self::LAST_PAGE_VIEW_DATE,
|
||||
self::LAST_SENDING_DATE,
|
||||
self::SUBSCRIBED_DATE,
|
||||
];
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
DateFilterHelper $dateFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$action = $filter->getFilterData()->getAction();
|
||||
$value = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('date');
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $value);
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
$columnName = $this->getColumnNameForAction($action);
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
$queryBuilder->andWhere("DATE($columnName) < :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andWhere("DATE($columnName) > :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
$queryBuilder->andWhere("DATE($columnName) = :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andWhere("DATE($columnName) <= :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andWhere("DATE($columnName) != :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andWhere("DATE($columnName) >= :$parameter");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($parameter, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getColumnNameForAction(string $action): string {
|
||||
switch ($action) {
|
||||
case self::LAST_CLICK_DATE:
|
||||
return 'last_click_at';
|
||||
case self::LAST_ENGAGEMENT_DATE:
|
||||
return 'last_engagement_at';
|
||||
case self::LAST_PURCHASE_DATE:
|
||||
return 'last_purchase_at';
|
||||
case self::LAST_OPEN_DATE:
|
||||
return 'last_open_at';
|
||||
case self::LAST_PAGE_VIEW_DATE:
|
||||
return 'last_page_view_at';
|
||||
case self::SUBSCRIBED_DATE:
|
||||
return 'last_subscribed_at';
|
||||
case self::LAST_SENDING_DATE:
|
||||
return 'last_sending_at';
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberScore implements Filter {
|
||||
const TYPE = 'subscriberScore';
|
||||
|
||||
const HIGHER_THAN = 'higherThan';
|
||||
const LOWER_THAN = 'lowerThan';
|
||||
const EQUALS = 'equals';
|
||||
const NOT_EQUALS = 'not_equals';
|
||||
const UNKNOWN = 'unknown';
|
||||
const NOT_UNKNOWN = 'not_unknown';
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$parameter = 'score' . $parameterSuffix;
|
||||
|
||||
if ($operator === self::HIGHER_THAN) {
|
||||
$queryBuilder->andWhere("engagement_score > :$parameter");
|
||||
} elseif ($operator === self::LOWER_THAN) {
|
||||
$queryBuilder->andWhere("engagement_score < :$parameter");
|
||||
} elseif ($operator === self::EQUALS) {
|
||||
$queryBuilder->andWhere("engagement_score = :$parameter");
|
||||
} elseif ($operator === self::NOT_EQUALS) {
|
||||
$queryBuilder->andWhere("engagement_score != :$parameter");
|
||||
} elseif ($operator === self::UNKNOWN) {
|
||||
$queryBuilder->andWhere("engagement_score IS NULL");
|
||||
} elseif ($operator === self::NOT_UNKNOWN) {
|
||||
$queryBuilder->andWhere("engagement_score IS NOT NULL");
|
||||
} else {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($parameter, (int)$value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class SubscriberSegment implements Filter {
|
||||
const TYPE = 'subscribedToList';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
SegmentsRepository $segmentsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$segments = $filterData->getParam('segments');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$statusSubscribedParam = 'subscribed' . $parameterSuffix;
|
||||
$segmentsParam = 'segments' . $parameterSuffix;
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$subscriberSegmentTable,
|
||||
'subscriber_segments',
|
||||
"$subscribersTable.id = subscriber_segments.subscriber_id"
|
||||
. ' AND subscriber_segments.status = :' . $statusSubscribedParam
|
||||
. ' AND subscriber_segments.segment_id IN (:' . $segmentsParam . ')'
|
||||
);
|
||||
|
||||
$queryBuilder->setParameter($statusSubscribedParam, SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
$queryBuilder->setParameter($segmentsParam, $segments, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->andWhere('subscriber_segments.id IS NULL');
|
||||
} else {
|
||||
$queryBuilder->andWhere('subscriber_segments.id IS NOT NULL');
|
||||
}
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($segments));
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'segments' => [],
|
||||
];
|
||||
$segmentIds = $filterData->getArrayParam('segments');
|
||||
$segments = $this->segmentsRepository->findBy(['id' => $segmentIds]);
|
||||
foreach ($segments as $segment) {
|
||||
$lookupData['segments'][$segment->getId()] = $segment->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsFormEntity;
|
||||
use MailPoet\Form\FormsRepository;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberSubscribedViaForm implements Filter {
|
||||
const TYPE = 'subscribedViaForm';
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var FormsRepository */
|
||||
private $formsRepository;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
FormsRepository $formsRepository
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->formsRepository = $formsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$formIds = $filterData->getParam('form_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$formStatsTable = $this->filterHelper->getTableForEntity(StatisticsFormEntity::class);
|
||||
|
||||
$formIdsParam = $this->filterHelper->getUniqueParameterName('formIds');
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$formStatsTable,
|
||||
'statisticsForms',
|
||||
"$subscribersTable.id = statisticsForms.subscriber_id"
|
||||
);
|
||||
$queryBuilder->andWhere("statisticsForms.form_id IN (:$formIdsParam)");
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$formStatsTable,
|
||||
'statisticsForms',
|
||||
"$subscribersTable.id = statisticsForms.subscriber_id AND statisticsForms.form_id IN (:$formIdsParam)"
|
||||
);
|
||||
$queryBuilder->andWhere("statisticsForms.subscriber_id IS NULL");
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($formIdsParam, $formIds, ArrayParameterType::INTEGER);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'forms' => [],
|
||||
];
|
||||
$formIds = $filterData->getArrayParam('form_ids');
|
||||
$forms = $this->formsRepository->findBy(['id' => $formIds]);
|
||||
foreach ($forms as $form) {
|
||||
$lookupData['forms'][$form->getId()] = $form->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* The filters in this class are primarily intended for the premium plugin
|
||||
*/
|
||||
class SubscriberTag implements Filter {
|
||||
const TYPE = 'subscriberTag';
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$this->wp->applyFilters('mailpoet_dynamic_segments_filter_subscriber_tag_apply', $queryBuilder, $filter);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$default = ['tags' => []];
|
||||
$filteredLookupData = $this->wp->applyFilters('mailpoet_dynamic_segments_filter_subscriber_tag_getLookupData', $default, $filterData);
|
||||
if (is_array($filteredLookupData)) {
|
||||
return $filteredLookupData;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberTextField implements Filter {
|
||||
const FIRST_NAME = 'subscriberFirstName';
|
||||
const LAST_NAME = 'subscriberLastName';
|
||||
const EMAIL = 'subscriberEmail';
|
||||
|
||||
const TYPES = [self::FIRST_NAME, self::LAST_NAME, self::EMAIL];
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if (!is_string($operator)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$columnName = $this->getColumnNameForAction($action);
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('subscriberText');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_IS:
|
||||
$queryBuilder->andWhere("$columnName = :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_IS_NOT:
|
||||
$queryBuilder->andWhere("$columnName != :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($parameter, $value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getColumnNameForAction(string $field): string {
|
||||
switch ($field) {
|
||||
case self::FIRST_NAME:
|
||||
return 'first_name';
|
||||
case self::LAST_NAME:
|
||||
return 'last_name';
|
||||
case self::EMAIL:
|
||||
return 'email';
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Invalid action');
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\InvalidStateException;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class UserRole implements Filter {
|
||||
const TYPE = 'wordpressRole';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$filterData = $filter->getFilterData();
|
||||
$role = $filterData->getParam('wordpressRole');
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!$role) {
|
||||
throw new InvalidFilterException('Missing role', InvalidFilterException::MISSING_ROLE);
|
||||
}
|
||||
if (!is_array($role)) {
|
||||
// compatibility with the older segment before multiple roles were added
|
||||
$role = [$role];
|
||||
}
|
||||
if (!$operator) {
|
||||
$operator = DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
}
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$parameterSuffix = ((string)$filter->getId()) . Security::generateRandomString();
|
||||
$condition = $this->createCondition($role, $operator, $parameterSuffix);
|
||||
$qb = $queryBuilder->join($subscribersTable, $wpdb->users, 'wpusers', "$subscribersTable.wp_user_id = wpusers.id")
|
||||
->join('wpusers', $wpdb->usermeta, 'wpusermeta', 'wpusers.id = wpusermeta.user_id')
|
||||
->andWhere("wpusermeta.meta_key = '{$wpdb->prefix}capabilities' AND (" . $condition . ')');
|
||||
foreach ($role as $key => $userRole) {
|
||||
$qb->setParameter('role' . $key . $parameterSuffix, '%"' . $userRole . '"%');
|
||||
}
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
* @param string $operator
|
||||
* @param string $parameterSuffix
|
||||
* @return string
|
||||
*/
|
||||
private function createCondition(array $roles, string $operator, $parameterSuffix): string {
|
||||
$sqlParts = [];
|
||||
foreach ($roles as $key => $role) {
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$sqlParts[] = '(wpusermeta.meta_value NOT LIKE :role' . $key . $parameterSuffix . ')';
|
||||
} else {
|
||||
$sqlParts[] = '(wpusermeta.meta_value LIKE :role' . $key . $parameterSuffix . ')';
|
||||
}
|
||||
}
|
||||
if (($operator === DynamicSegmentFilterData::OPERATOR_NONE) || ($operator === DynamicSegmentFilterData::OPERATOR_ALL)) {
|
||||
return join(' AND ', $sqlParts);
|
||||
}
|
||||
return join(' OR ', $sqlParts);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
global $wp_roles;
|
||||
$lookupData = [
|
||||
'roles' => [],
|
||||
];
|
||||
$roles = $filterData->getParam('wordpressRole');
|
||||
if (is_string($roles)) {
|
||||
$roles = [$roles];
|
||||
}
|
||||
if (!is_array($roles)) {
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
foreach ($roles as $roleSlug) {
|
||||
$roleData = $wp_roles->roles[$roleSlug] ?? null;
|
||||
if (is_array($roleData)) {
|
||||
$lookupData['roles'][$roleSlug] = $roleData['name'];
|
||||
}
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceAverageSpent implements Filter {
|
||||
const ACTION = 'averageSpent';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('average_spent_type');
|
||||
$amount = $filterData->getParam('average_spent_amount');
|
||||
$timeframe = $filterData->getParam('timeframe');
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if ($timeframe !== DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
/** @var int $days */
|
||||
$days = $filterData->getParam('days');
|
||||
$days = intval($days);
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
$amountParam = $this->filterHelper->getUniqueParameterName('amount');
|
||||
if ($operator === '=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) = :$amountParam");
|
||||
} elseif ($operator === '!=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) != :$amountParam");
|
||||
} elseif ($operator === '>') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) > :$amountParam");
|
||||
} elseif ($operator === '<') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) < :$amountParam");
|
||||
} elseif ($operator === '<=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) <= :$amountParam");
|
||||
} elseif ($operator === '>=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) >= :$amountParam");
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($amountParam, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use WP_Term;
|
||||
|
||||
class WooCommerceCategory implements Filter {
|
||||
const ACTION_CATEGORY = 'purchasedCategory';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->wp = $wp;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
|
||||
$operator = $filterData->getOperator();
|
||||
$categoryIds = (array)$filterData->getParam('category_ids');
|
||||
$categoryIdswithChildrenIds = $this->getCategoriesWithChildren($categoryIds);
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$parameterSuffix = (string)$parameterSuffix;
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$this->applyTermRelationshipsJoin($queryBuilder);
|
||||
$this->applyTermTaxonomyJoin($queryBuilder, $parameterSuffix);
|
||||
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$subQueryCount = 1;
|
||||
foreach ($categoryIds as $categoryId) {
|
||||
$uniqueParamaterSuffix = Security::generateRandomString();
|
||||
$categoryIdWithChildrenIds = $this->getCategoriesWithChildren([$categoryId]);
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$this->applyTermRelationshipsJoin($subQuery);
|
||||
$this->applyTermTaxonomyJoin($subQuery, $uniqueParamaterSuffix);
|
||||
$subQuery->setParameter("category_$uniqueParamaterSuffix", $categoryIdWithChildrenIds, ArrayParameterType::STRING);
|
||||
$alias = sprintf("subQuery%s", $subQueryCount);
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf("(%s)", $this->filterHelper->getInterpolatedSQL($subQuery)),
|
||||
$alias,
|
||||
"$subscribersTable.id = $alias.id"
|
||||
);
|
||||
$subQueryCount++;
|
||||
}
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
// subQuery with subscriber ids that bought products
|
||||
$subQuery = $this->createQueryBuilder($subscribersTable);
|
||||
$subQuery->select("DISTINCT $subscribersTable.id");
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$subQuery = $this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$subQuery = $this->applyTermRelationshipsJoin($subQuery);
|
||||
$subQuery = $this->applyTermTaxonomyJoin($subQuery, $parameterSuffix);
|
||||
// apply subQuery for negation
|
||||
$queryBuilder->where("$subscribersTable.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->setParameter("category_$parameterSuffix", $categoryIdswithChildrenIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$wpdb->prefix . 'wc_order_product_lookup',
|
||||
'product',
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyTermRelationshipsJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->join(
|
||||
'product',
|
||||
$wpdb->term_relationships, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'term_relationships',
|
||||
'product.product_id = term_relationships.object_id'
|
||||
);
|
||||
}
|
||||
|
||||
private function applyTermTaxonomyJoin(QueryBuilder $queryBuilder, string $parameterSuffix): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'term_relationships',
|
||||
$wpdb->term_taxonomy, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'term_taxonomy',
|
||||
"term_taxonomy.term_taxonomy_id=term_relationships.term_taxonomy_id
|
||||
AND
|
||||
term_taxonomy.term_id IN (:category_$parameterSuffix)"
|
||||
);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(string $table): QueryBuilder {
|
||||
return $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($table);
|
||||
}
|
||||
|
||||
private function getCategoriesWithChildren(array $categoriesId): array {
|
||||
$allIds = [];
|
||||
|
||||
foreach ($categoriesId as $categoryId) {
|
||||
$allIds = array_merge($allIds, $this->getAllCategoryIds($categoryId));
|
||||
}
|
||||
|
||||
return array_unique($allIds);
|
||||
}
|
||||
|
||||
private function getAllCategoryIds(int $categoryId): array {
|
||||
$subcategories = $this->wp->getTerms(['taxonomy' => 'product_cat', 'child_of' => $categoryId, 'hide_empty' => false]);
|
||||
if (!is_array($subcategories) || empty($subcategories)) {
|
||||
return [$categoryId];
|
||||
}
|
||||
$ids = array_map(function($category) {
|
||||
return $category->term_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}, $subcategories);
|
||||
$ids[] = $categoryId;
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'categories' => [],
|
||||
];
|
||||
$categoryIds = $filterData->getArrayParam('category_ids');
|
||||
$terms = $this->wp->getTerms(['taxonomy' => 'product_cat', 'include' => $categoryIds, 'hide_empty' => false]);
|
||||
/** @var WP_Term[] $terms */
|
||||
foreach ($terms as $term) {
|
||||
$lookupData['categories'][$term->term_id] = $term->name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceCountry implements Filter {
|
||||
const ACTION_CUSTOMER_COUNTRY = 'customerInCountry';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$filterData = $filter->getFilterData();
|
||||
$countryCode = $filterData->getParam('country_code');
|
||||
if (!is_array($countryCode)) {
|
||||
$countryCode = [(string)$countryCode];
|
||||
}
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!$operator) {
|
||||
$operator = DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
}
|
||||
|
||||
$countryFilterParam = ((string)$filter->getId()) . Security::generateRandomString();
|
||||
$condition = $this->createCondition($countryCode, $operator, $countryFilterParam);
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'email'
|
||||
);
|
||||
$qb = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'customer',
|
||||
"$subscribersTable.email = customer.email $collation"
|
||||
)->where($condition);
|
||||
|
||||
foreach ($countryCode as $key => $userCountryCode) {
|
||||
$qb->setParameter('countryCode' . $key . $countryFilterParam, '%' . $userCountryCode . '%');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function createCondition(array $countryCodes, string $operator, string $countryFilterParam): string {
|
||||
$sqlParts = [];
|
||||
foreach ($countryCodes as $key => $userCountryCode) {
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$sqlParts[] = '(customer.country NOT LIKE :countryCode' . $key . $countryFilterParam . ')';
|
||||
} else {
|
||||
$sqlParts[] = '(customer.country LIKE :countryCode' . $key . $countryFilterParam . ')';
|
||||
}
|
||||
}
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
return join(' AND ', $sqlParts);
|
||||
}
|
||||
return join(' OR ', $sqlParts);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceCustomerTextField implements Filter {
|
||||
const CITY = 'customerInCity';
|
||||
const POSTAL_CODE = 'customerInPostalCode';
|
||||
|
||||
const ACTIONS = [self::CITY, self::POSTAL_CODE];
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if (!is_string($operator)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$customerLookupAlias = $this->wooFilterHelper->applyCustomerLookupJoin($queryBuilder);
|
||||
$column = sprintf("%s.%s", $customerLookupAlias, $this->getColumnNameForAction($action));
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('customerTextField');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_IS:
|
||||
$queryBuilder->andWhere("$column = :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_IS_NOT:
|
||||
$queryBuilder->andWhere("$column != :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($parameter, $value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getColumnNameForAction(string $field): string {
|
||||
switch ($field) {
|
||||
case self::CITY:
|
||||
return 'city';
|
||||
case self::POSTAL_CODE:
|
||||
return 'postcode';
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Invalid action');
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceFirstOrder implements Filter {
|
||||
const ACTION = 'firstOrder';
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$dateValue = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $dateValue);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if (in_array($operator, [DateFilterHelper::NOT_ON, DateFilterHelper::NOT_IN_THE_LAST])) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $subQuery);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("{$subscribersTable}.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
} else {
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $queryBuilder);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyConditionsToQueryBuilder(string $operator, string $date, QueryBuilder $queryBuilder): QueryBuilder {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) < :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) > :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) >= :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) = :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) <= :$dateParam");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($dateParam, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceMembership implements Filter {
|
||||
const ACTION_MEMBER_OF = 'isMemberOf';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
/** @var array */
|
||||
$planIds = $filterData->getParam('plan_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
// ALL OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyPostJoin($queryBuilder);
|
||||
$this->applyParentPostJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")")
|
||||
->groupBy("$subscribersTable.id")
|
||||
->having("COUNT($subscribersTable.id) = :count$parameterSuffix")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING)
|
||||
->setParameter('count' . $parameterSuffix, count($planIds));
|
||||
}
|
||||
|
||||
// NONE OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQueryBuilder = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscribersTable)
|
||||
->select("DISTINCT $subscribersTable.id");
|
||||
$this->applyPostJoin($subQueryBuilder);
|
||||
$this->applyParentPostJoin($subQueryBuilder);
|
||||
$subQueryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")");
|
||||
return $queryBuilder->where("{$subscribersTable}.id NOT IN ({$subQueryBuilder->getSQL()})")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
// ANY
|
||||
$this->applyPostJoin($queryBuilder);
|
||||
$this->applyParentPostJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->posts,
|
||||
'posts',
|
||||
"posts.post_type = 'wc_user_membership' AND posts.post_status IN ('wcm-active', 'wcm-complimentary', 'wcm-free_trial', 'wcm-pending') AND posts.post_author=$subscribersTable.wp_user_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyParentPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'posts',
|
||||
$wpdb->posts,
|
||||
'parentposts',
|
||||
"posts.post_parent = parentposts.id AND parentposts.post_type = 'wc_membership_plan' AND parentposts.post_status = 'publish'"
|
||||
);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceNumberOfOrders implements Filter {
|
||||
const ACTION_NUMBER_OF_ORDERS = 'numberOfOrders';
|
||||
const ACTION_NUMBER_OF_ORDERS_WITH_COUPON = 'numberOfOrdersWithCoupon';
|
||||
|
||||
const ACTIONS = [
|
||||
self::ACTION_NUMBER_OF_ORDERS,
|
||||
self::ACTION_NUMBER_OF_ORDERS_WITH_COUPON,
|
||||
];
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$filterData = $filter->getFilterData();
|
||||
/** @var string $type - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$type = $filterData->getParam('number_of_orders_type');
|
||||
$type = strval($type);
|
||||
/** @var string $count - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$count = $filterData->getParam('number_of_orders_count');
|
||||
$count = intval($count);
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'email'
|
||||
);
|
||||
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
|
||||
$joinCondition = $isAllTime
|
||||
? 'customer.customer_id = orderStats.customer_id AND orderStats.status IN (:allowedStatuses' . $parameterSuffix . ')'
|
||||
: 'customer.customer_id = orderStats.customer_id AND orderStats.date_created >= :date' . $parameterSuffix . ' AND orderStats.status IN (:allowedStatuses' . $parameterSuffix . ')';
|
||||
|
||||
$subQuery = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($wpdb->prefix . 'wc_customer_lookup', "customer")
|
||||
->select("customer.email $collation as email")
|
||||
->addSelect("orderStats.order_id as oder_stats_id")
|
||||
->leftJoin(
|
||||
'customer',
|
||||
$wpdb->prefix . 'wc_order_stats',
|
||||
'orderStats',
|
||||
$joinCondition
|
||||
);
|
||||
|
||||
$action = $filterData->getAction();
|
||||
|
||||
if ($action === self::ACTION_NUMBER_OF_ORDERS_WITH_COUPON) {
|
||||
$subQuery->innerJoin('orderStats', $wpdb->prefix . 'wc_order_coupon_lookup', 'couponLookup', 'orderStats.order_id = couponLookup.order_id');
|
||||
}
|
||||
|
||||
$queryBuilder->add('join', [
|
||||
$subscribersTable => [
|
||||
/**
|
||||
* Based the combination of $type and $count we may need to include none-customer subscribers
|
||||
* in this case we'll need to leftJoin subscribers table to result of the sub-query defined above,
|
||||
* in all other cases innerJoin gets us the expected records.
|
||||
*/
|
||||
'joinType' => $this-> shouldIncludeNoneCustomerSubscribers($type, $count) ? 'left' : 'inner',
|
||||
'joinTable' => "({$subQuery->getSQL()})",
|
||||
'joinAlias' => 'selectedCustomers',
|
||||
'joinCondition' => "$subscribersTable.email = selectedCustomers.email $collation",
|
||||
],
|
||||
], \true)
|
||||
->setParameter('date' . $parameterSuffix, $date->toDateTimeString())
|
||||
->setParameter('allowedStatuses' . $parameterSuffix, $this->wooFilterHelper->defaultIncludedStatuses(), ArrayParameterType::STRING)
|
||||
->groupBy('inner_subscriber_id');
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) = :count' . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) != :count' . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) > :count' . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) < :count' . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('count' . $parameterSuffix, $count, 'integer');
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function shouldIncludeNoneCustomerSubscribers(string $type, int $count): bool {
|
||||
if ($type === '=') {
|
||||
return $count === 0;
|
||||
} elseif ($type === '!=') {
|
||||
return true;
|
||||
} elseif ($type === '>') {
|
||||
return $count < 0;
|
||||
} elseif ($type === '<') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceNumberOfReviews implements Filter {
|
||||
const ACTION = 'numberOfReviews';
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
DBCollationChecker $collationChecker,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$commentsTable = $this->filterHelper->getPrefixedTable('comments');
|
||||
$commentMetaTable = $this->filterHelper->getPrefixedTable('commentmeta');
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
/** @var string $type - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$type = $filterData->getParam('count_type');
|
||||
$type = strval($type);
|
||||
/** @var string $rating - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$rating = $filterData->getParam('rating');
|
||||
$rating = strval($rating);
|
||||
/** @var int $days - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$days = $filterData->getParam('days');
|
||||
$days = intval($days);
|
||||
/** @var int $count - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$count = $filterData->getParam('count');
|
||||
$count = intval($count);
|
||||
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$commentsTable,
|
||||
'comment_author_email'
|
||||
);
|
||||
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$joinCondition = "$subscribersTable.email = comments.comment_author_email $collation
|
||||
AND comments.comment_type = 'review'";
|
||||
|
||||
if (!$isAllTime) {
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$joinCondition .= " AND comments.comment_date >= :$dateParam";
|
||||
$queryBuilder->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$commentMetaJoinCondition = "comments.comment_ID = commentmeta.comment_id AND commentmeta.meta_key = 'rating'";
|
||||
|
||||
if ($rating !== 'any') {
|
||||
$ratingParam = $this->filterHelper->getUniqueParameterName('rating');
|
||||
$commentMetaJoinCondition .= "AND commentmeta.meta_value = :$ratingParam";
|
||||
$queryBuilder->setParameter($ratingParam, $rating);
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->leftJoin(
|
||||
$subscribersTable,
|
||||
$commentsTable,
|
||||
'comments',
|
||||
$joinCondition
|
||||
)->leftJoin(
|
||||
'comments',
|
||||
$commentMetaTable,
|
||||
'commentmeta',
|
||||
$commentMetaJoinCondition
|
||||
);
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
|
||||
switch ($type) {
|
||||
case '=':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) = :$countParam");
|
||||
break;
|
||||
case '!=':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) != :$countParam");
|
||||
break;
|
||||
case '>':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) > :$countParam");
|
||||
break;
|
||||
case '<':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) < :$countParam");
|
||||
break;
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($countParam, $count, 'integer');
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
if (!isset($data['rating'])) {
|
||||
throw new InvalidFilterException('Missing rating', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$validRatings = ['1', '2', '3', '4', '5', 'any'];
|
||||
if (!in_array($data['rating'], $validRatings, true)) {
|
||||
throw new InvalidFilterException('Invalid rating', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['count_type'])) {
|
||||
throw new InvalidFilterException('Missing count type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$type = $data['count_type'];
|
||||
$validTypes = [
|
||||
'=',
|
||||
'!=',
|
||||
'>',
|
||||
'<',
|
||||
];
|
||||
if (!in_array($type, $validTypes, true)) {
|
||||
throw new InvalidFilterException('Invalid count type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
if (!isset($data['count'])) {
|
||||
throw new InvalidFilterException('Missing review count', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use WC_Product;
|
||||
|
||||
class WooCommerceProduct implements Filter {
|
||||
const ACTION_PRODUCT = 'purchasedProduct';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
Helper $wooHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getOperator();
|
||||
$productIds = $filterData->getParam('product_ids');
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->andWhere("product.product_id IN (:products_{$parameterSuffix})");
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->andWhere("product.product_id IN (:products_{$parameterSuffix})")
|
||||
->groupBy("{$subscribersTable}.id, $orderStatsAlias.order_id")
|
||||
->having("COUNT($orderStatsAlias.order_id) = :count" . $parameterSuffix)
|
||||
->setParameter('count' . $parameterSuffix, count($productIds));
|
||||
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
// subQuery with subscriber ids that bought products
|
||||
$subQuery = $this->createQueryBuilder($subscribersTable);
|
||||
$subQuery->select("DISTINCT $subscribersTable.id");
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$subQuery = $this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$subQuery->andWhere("product.product_id IN (:products_{$parameterSuffix})");
|
||||
// application subQuery for negation
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
return $queryBuilder
|
||||
->setParameter("products_{$parameterSuffix}", $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$wpdb->prefix . 'wc_order_product_lookup',
|
||||
'product',
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(string $table): QueryBuilder {
|
||||
return $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($table);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['products' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$productIds = $filterData->getArrayParam('product_ids');
|
||||
foreach ($productIds as $productId) {
|
||||
$product = $this->wooHelper->wcGetProduct($productId);
|
||||
if ($product instanceof WC_Product) {
|
||||
$lookupData['products'][$productId] = $product->get_name();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommercePurchaseDate implements Filter {
|
||||
const ACTION = 'purchaseDate';
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$dateValue = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $dateValue);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if (in_array($operator, [DateFilterHelper::NOT_ON, DateFilterHelper::NOT_IN_THE_LAST])) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $subQuery);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("{$subscribersTable}.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
} else {
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $queryBuilder);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyConditionsToQueryBuilder(string $operator, string $date, QueryBuilder $queryBuilder): QueryBuilder {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) < :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) > :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) >= :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) = :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) <= :$dateParam");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($dateParam, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WP\Functions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommercePurchasedWithAttribute implements Filter {
|
||||
const ACTION = 'purchasedWithAttribute';
|
||||
|
||||
const TYPE_LOCAL = 'local';
|
||||
const TYPE_TAXONOMY = 'taxonomy';
|
||||
|
||||
private WooFilterHelper $wooFilterHelper;
|
||||
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
private Functions $wp;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Functions $wp
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$type = $filterData->getStringParam('attribute_type');
|
||||
|
||||
if ($type === self::TYPE_LOCAL) {
|
||||
$this->applyForLocalAttribute($queryBuilder, $filterData);
|
||||
} elseif ($type === self::TYPE_TAXONOMY) {
|
||||
$this->applyForTaxonomyAttribute($queryBuilder, $filterData);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForTaxonomyAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$attributeTaxonomySlug = $filterData->getStringParam('attribute_taxonomy_slug');
|
||||
$attributeTermIds = $filterData->getArrayParam('attribute_term_ids');
|
||||
$termIdsParam = $this->filterHelper->getUniqueParameterName('attribute_term_ids');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$attributeAlias = $this->applyTaxonomyAttributeJoin($queryBuilder, $productAlias, $attributeTaxonomySlug);
|
||||
$queryBuilder->andWhere("$attributeAlias.term_id IN (:$termIdsParam)");
|
||||
$queryBuilder->setParameter($termIdsParam, $attributeTermIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias, string $alias = 'product'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_product_lookup'),
|
||||
$alias,
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
return $alias;
|
||||
}
|
||||
|
||||
private function applyTaxonomyAttributeJoin(QueryBuilder $queryBuilder, string $productAlias, $taxonomySlug, string $alias = 'attribute'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_product_attributes_lookup'),
|
||||
$alias,
|
||||
"product.product_id = attribute.product_id AND attribute.taxonomy = '$taxonomySlug'"
|
||||
);
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$type = $filterData->getStringParam('attribute_type');
|
||||
|
||||
if ($type !== self::TYPE_TAXONOMY) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$slug = $filterData->getStringParam('attribute_taxonomy_slug');
|
||||
|
||||
$lookupData = [
|
||||
'attribute' => $slug,
|
||||
];
|
||||
|
||||
$termIds = $filterData->getArrayParam('attribute_term_ids');
|
||||
$terms = $this->wp->getTerms([
|
||||
'taxonomy' => $slug,
|
||||
'include' => $termIds,
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
$lookupData['terms'] = array_map(function($term) {
|
||||
return $term->name;
|
||||
}, $terms);
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$operator = $data['operator'] ?? null;
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$this->validateAttributeData($data);
|
||||
}
|
||||
|
||||
public function validateAttributeData(array $data): void {
|
||||
$type = $data['attribute_type'];
|
||||
|
||||
if (!in_array($type, [self::TYPE_LOCAL, self::TYPE_TAXONOMY], true)) {
|
||||
throw new InvalidFilterException('Invalid attribute type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_LOCAL) {
|
||||
$name = $data['attribute_local_name'] ?? null;
|
||||
if (!is_string($name) || strlen($name) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$values = $data['attribute_local_values'] ?? [];
|
||||
if (!is_array($values) || count($values) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute values', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_TAXONOMY) {
|
||||
$attribute_taxonomy_slug = $data['attribute_taxonomy_slug'] ?? null;
|
||||
if (!is_string($attribute_taxonomy_slug) || strlen($attribute_taxonomy_slug) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['attribute_term_ids']) || !is_array($data['attribute_term_ids']) || count($data['attribute_term_ids']) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute terms', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForTaxonomyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData) {
|
||||
$operator = $filterData->getOperator();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT attribute.term_id) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('attribute_term_ids')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForTaxonomyAnyOperator($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForLocalAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$operator = $filterData->getOperator();
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT postmeta.meta_value) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('attribute_local_values')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForLocalAnyAttribute($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForLocalAnyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$attributeName = $filterData->getStringParam('attribute_local_name');
|
||||
$attributeValues = $filterData->getArrayParam('attribute_local_values');
|
||||
$valuesParam = $this->filterHelper->getUniqueParameterName('attribute_values');
|
||||
$keyParam = $this->filterHelper->getUniqueParameterName('attribute_name');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('postmeta'),
|
||||
'postmeta',
|
||||
"$productAlias.product_id = postmeta.post_id AND postmeta.meta_key = :$keyParam AND postmeta.meta_value IN (:$valuesParam)"
|
||||
);
|
||||
|
||||
$queryBuilder->setParameter($keyParam, sprintf("attribute_%s", $attributeName));
|
||||
$queryBuilder->setParameter($valuesParam, $attributeValues, ArrayParameterType::STRING);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceSingleOrderValue implements Filter {
|
||||
const ACTION_SINGLE_ORDER_VALUE = 'singleOrderValue';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$type = $filterData->getParam('single_order_value_type');
|
||||
$amount = $filterData->getParam('single_order_value_amount');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
$days = $filterData->getParam('days');
|
||||
if (!is_string($days)) {
|
||||
$days = '1'; // Default to last day
|
||||
}
|
||||
$date = Carbon::now()->subDays((int)$days);
|
||||
$dateParam = "date_$parameterSuffix";
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales = :amount" . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales != :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales > :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales >= :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales < :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales <= :amount" . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('amount' . $parameterSuffix, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceSubscription implements Filter {
|
||||
const ACTION_HAS_ACTIVE = 'hasActiveSubscription';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $woocommerceHelper;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker,
|
||||
WooCommerceHelper $woocommerceHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$productIds = $filterData->getParam('product_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
// ALL OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyPostmetaAndPostJoin($queryBuilder);
|
||||
$this->applyOrderItemsJoin($queryBuilder);
|
||||
$this->applyOrderItemmetaJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")")
|
||||
->groupBy("$subscribersTable.id")
|
||||
->having("COUNT($subscribersTable.id) = :count$parameterSuffix")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING)
|
||||
->setParameter('count' . $parameterSuffix, count($productIds));
|
||||
}
|
||||
|
||||
// NONE OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQueryBuilder = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscribersTable)
|
||||
->select("DISTINCT $subscribersTable.id");
|
||||
$this->applyPostmetaAndPostJoin($subQueryBuilder);
|
||||
$this->applyOrderItemsJoin($subQueryBuilder);
|
||||
$this->applyOrderItemmetaJoin($subQueryBuilder);
|
||||
$subQueryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")");
|
||||
return $queryBuilder->where("{$subscribersTable}.id NOT IN ({$subQueryBuilder->getSQL()})")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
// ANY
|
||||
$this->applyPostmetaAndPostJoin($queryBuilder);
|
||||
$this->applyOrderItemsJoin($queryBuilder);
|
||||
$this->applyOrderItemmetaJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyPostmetaAndPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
'billing_email'
|
||||
);
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
'wc_orders',
|
||||
"{$subscribersTable}.email = wc_orders.billing_email $collation AND wc_orders.status IN('wc-active', 'wc-pending-cancel')"
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->postmeta,
|
||||
'postmeta',
|
||||
"postmeta.meta_key = '_customer_user' AND $subscribersTable.wp_user_id=postmeta.meta_value"
|
||||
)->innerJoin(
|
||||
'postmeta',
|
||||
$wpdb->posts,
|
||||
'posts',
|
||||
"postmeta.post_id = posts.id AND posts.post_type = 'shop_subscription' AND posts.post_status IN('wc-active', 'wc-pending-cancel')"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyOrderItemsJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
return $queryBuilder->innerJoin(
|
||||
'wc_orders',
|
||||
$wpdb->prefix . 'woocommerce_order_items',
|
||||
'items',
|
||||
"wc_orders.id = items.order_id AND order_item_type = 'line_item'"
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
'postmeta',
|
||||
$wpdb->prefix . 'woocommerce_order_items',
|
||||
'items',
|
||||
"postmeta.post_id = items.order_id AND order_item_type = 'line_item'"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyOrderItemmetaJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'items',
|
||||
$wpdb->prefix . 'woocommerce_order_itemmeta',
|
||||
'itemmeta',
|
||||
"itemmeta.order_item_id=items.order_item_id AND itemmeta.meta_key='_product_id'"
|
||||
);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use WP_Term;
|
||||
|
||||
class WooCommerceTag implements Filter {
|
||||
const ACTION = 'purchasedTag';
|
||||
|
||||
private WPFunctions $wp;
|
||||
private WooFilterHelper $wooFilterHelper;
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$operator = $filterData->getOperator();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForAnyOperator($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForAnyOperator($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('tagCount');
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT term_taxonomy.term_id) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('tag_ids')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function applyForAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$tagIdsParam = $this->filterHelper->getUniqueParameterName('tagIds');
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->join(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('term_relationships'),
|
||||
'term_relationships',
|
||||
'product.product_id = term_relationships.object_id'
|
||||
);
|
||||
$queryBuilder->innerJoin(
|
||||
'term_relationships',
|
||||
$this->filterHelper->getPrefixedTable('term_taxonomy'),
|
||||
'term_taxonomy',
|
||||
"term_taxonomy.term_taxonomy_id = term_relationships.term_taxonomy_id
|
||||
AND
|
||||
term_taxonomy.term_id IN (:$tagIdsParam)"
|
||||
);
|
||||
$queryBuilder->setParameter($tagIdsParam, $filterData->getArrayParam('tag_ids'), ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias, string $productAlias = 'product'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_product_lookup'),
|
||||
$productAlias,
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
return $productAlias;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$operator = $data['operator'] ?? null;
|
||||
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($data['tag_ids'] ?? null) || count($data['tag_ids']) === 0) {
|
||||
throw new InvalidFilterException('Missing tag ids');
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'tags' => [],
|
||||
];
|
||||
$tagIds = $filterData->getArrayParam('tag_ids');
|
||||
$terms = $this->wp->getTerms(['taxonomy' => 'product_tag', 'include' => $tagIds, 'hide_empty' => false]);
|
||||
/** @var WP_Term[] $terms */
|
||||
foreach ($terms as $term) {
|
||||
$lookupData['tags'][$term->term_id] = $term->name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceTotalSpent implements Filter {
|
||||
const ACTION_TOTAL_SPENT = 'totalSpent';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$type = $filterData->getParam('total_spent_type');
|
||||
$amount = $filterData->getParam('total_spent_amount');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = "date_$parameterSuffix";
|
||||
$queryBuilder->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) = :amount" . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) != :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) > :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) < :amount" . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('amount' . $parameterSuffix, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceUsedCouponCode implements Filter {
|
||||
const ACTION = 'usedCouponCode';
|
||||
|
||||
const COUPON_CODE_IDS_KEY = 'coupon_code_ids';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $filter);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $filter);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $filter);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): void {
|
||||
$filterData = $filter->getFilterData();
|
||||
$couponIds = (array)$filterData->getParam(self::COUPON_CODE_IDS_KEY);
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
/** @var int $days */
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_coupon_lookup'),
|
||||
'couponLookup',
|
||||
"$orderStatsAlias.order_id = couponLookup.order_id"
|
||||
);
|
||||
$couponCodeIdsParam = $this->filterHelper->getUniqueParameterName('couponCodeIds');
|
||||
$queryBuilder
|
||||
->andWhere("couponLookup.coupon_id IN (:$couponCodeIdsParam)")
|
||||
->setParameter($couponCodeIdsParam, $couponIds, ArrayParameterType::INTEGER);
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): void {
|
||||
$this->applyForAnyOperator($queryBuilder, $filter);
|
||||
|
||||
$filterData = $filter->getFilterData();
|
||||
$couponIds = (array)$filterData->getParam(self::COUPON_CODE_IDS_KEY);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT couponLookup.coupon_id) = " . count(array_unique($couponIds)));
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$couponCodeIds = $data[self::COUPON_CODE_IDS_KEY] ?? [];
|
||||
if (count($couponCodeIds) === 0) {
|
||||
throw new InvalidFilterException('Missing coupon code IDs', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$operator = $data['operator'] ?? null;
|
||||
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['coupons' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$couponIds = $filterData->getArrayParam(self::COUPON_CODE_IDS_KEY);
|
||||
foreach ($couponIds as $couponId) {
|
||||
$couponCode = $this->wooHelper->wcGetCouponCodeById((int)$couponId);
|
||||
if (!empty($couponCode)) {
|
||||
$lookupData['coupons'][$couponId] = $couponCode;
|
||||
}
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use WC_Payment_Gateway;
|
||||
|
||||
class WooCommerceUsedPaymentMethod implements Filter {
|
||||
const ACTION = 'usedPaymentMethod';
|
||||
|
||||
const VALID_OPERATORS = [
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
];
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
$paymentMethods = $filterData->getParam('payment_methods');
|
||||
$days = $filterData->getParam('days');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($paymentMethods) || count($paymentMethods) < 1) {
|
||||
throw new InvalidFilterException('Missing payment methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$data = $filterData->getData();
|
||||
$this->filterHelper->validateDaysPeriodData((array)$data);
|
||||
|
||||
$includedStatuses = array_keys($this->wooHelper->getOrderStatuses());
|
||||
$failedKey = array_search('wc-failed', $includedStatuses, true);
|
||||
if ($failedKey !== false) {
|
||||
unset($includedStatuses[$failedKey]);
|
||||
}
|
||||
$date = is_int($days) ? Carbon::now()->subDays($days) : Carbon::now();
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime): void {
|
||||
if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
} else {
|
||||
$this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime): void {
|
||||
if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$ordersAlias = $this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT $ordersAlias.payment_method) = " . count($paymentMethods));
|
||||
} else {
|
||||
$postmetaAlias = $this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')->having("COUNT(DISTINCT $postmetaAlias.meta_value) = " . count($paymentMethods));
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPostmetaOrderJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime, string $postmetaAlias = 'postmeta'): string {
|
||||
$paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
$paymentMethodMetaKeyParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
|
||||
$postMetaTable = $this->filterHelper->getPrefixedTable('postmeta');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $postMetaTable, $postmetaAlias, "$orderStatsAlias.order_id = $postmetaAlias.post_id")
|
||||
->andWhere("postmeta.meta_key = :$paymentMethodMetaKeyParam")
|
||||
->andWhere("postmeta.meta_value IN (:$paymentMethodParam)")
|
||||
->setParameter($paymentMethodMetaKeyParam, '_payment_method')
|
||||
->setParameter($paymentMethodParam, $paymentMethods, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
return $postmetaAlias;
|
||||
}
|
||||
|
||||
private function applyCustomOrderTableJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime, string $ordersAlias = 'orders'): string {
|
||||
$paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
$ordersTable = $this->wooHelper->getOrdersTableName();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $ordersTable, 'orders', "$orderStatsAlias.order_id = orders.id")
|
||||
->andWhere("$ordersAlias.payment_method IN (:$paymentMethodParam)")
|
||||
->setParameter($paymentMethodParam, $paymentMethods, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
return $ordersAlias;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'paymentMethods' => [],
|
||||
];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$paymentMethods = $filterData->getArrayParam('payment_methods');
|
||||
$allGateways = $this->wooHelper->getPaymentGateways()->payment_gateways();
|
||||
|
||||
foreach ($paymentMethods as $paymentMethod) {
|
||||
if (isset($allGateways[$paymentMethod]) && $allGateways[$paymentMethod] instanceof WC_Payment_Gateway) {
|
||||
$lookupData['paymentMethods'][$paymentMethod] = $allGateways[$paymentMethod]->get_method_title();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceUsedShippingMethod implements Filter {
|
||||
const ACTION = 'usedShippingMethod';
|
||||
|
||||
const VALID_OPERATORS = [
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
];
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
$shippingMethodInstanceIds = $filterData->getParam('shipping_methods');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$days = $filterData->getParam('days');
|
||||
|
||||
if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($shippingMethodInstanceIds) || empty($shippingMethodInstanceIds)) {
|
||||
throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$data = $filterData->getData();
|
||||
$this->filterHelper->validateDaysPeriodData((array)$data);
|
||||
|
||||
$includedStatuses = array_keys($this->wooHelper->getOrderStatuses());
|
||||
$failedKey = array_search('wc-failed', $includedStatuses, true);
|
||||
if ($failedKey !== false) {
|
||||
unset($includedStatuses[$failedKey]);
|
||||
}
|
||||
$date = is_int($days) ? Carbon::now()->subDays($days) : Carbon::now();
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$this->applyForNoneOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['shippingMethods' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$allMethods = $this->wooHelper->getShippingMethodInstancesData();
|
||||
$configuredShippingMethodInstanceIds = $filterData->getArrayParam('shipping_methods');
|
||||
|
||||
foreach ($configuredShippingMethodInstanceIds as $instanceId) {
|
||||
if (isset($allMethods[$instanceId])) {
|
||||
$data = $allMethods[$instanceId];
|
||||
$lookupData['shippingMethods'][$instanceId] = $data['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
|
||||
|
||||
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
|
||||
$orderItemsTableAlias = 'orderItems';
|
||||
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
|
||||
$orderItemMetaTableAlias = 'orderItemMeta';
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
|
||||
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
|
||||
->andWhere("$orderItemsTableAlias.order_item_type = 'shipping'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
|
||||
->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$orderItemTypeParam = $this->filterHelper->getUniqueParameterName('orderItemType');
|
||||
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
|
||||
|
||||
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
|
||||
$orderItemsTableAlias = 'orderItems';
|
||||
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
|
||||
$orderItemMetaTableAlias = 'orderItemMeta';
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
|
||||
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
|
||||
->andWhere("$orderItemsTableAlias.order_item_type = :$orderItemTypeParam")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
|
||||
->setParameter($orderItemTypeParam, 'shipping')
|
||||
->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING)
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT($orderItemMetaTableAlias.meta_value)) = " . count($shippingMethodInstanceIds));
|
||||
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForNoneOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooFilterHelper {
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
DBCollationChecker $collationChecker,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function defaultIncludedStatuses(): array {
|
||||
return ['wc-processing', 'wc-completed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param string $customerAlias
|
||||
* @return string - The alias of the joined customer lookup table
|
||||
*/
|
||||
public function applyCustomerLookupJoin(QueryBuilder $queryBuilder, string $customerAlias = 'customer'): string {
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$this->customerLookupTable(),
|
||||
'email'
|
||||
);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$this->customerLookupTable(),
|
||||
$customerAlias,
|
||||
"$subscribersTable.email = $customerAlias.email $collation"
|
||||
);
|
||||
|
||||
return $customerAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param string $orderStatsAlias
|
||||
* @return string - The alias of the joined order stats table
|
||||
*/
|
||||
public function applyCustomerOrderJoin(QueryBuilder $queryBuilder, string $orderStatsAlias = 'orderStats'): string {
|
||||
$customerAlias = $this->applyCustomerLookupJoin($queryBuilder);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$customerAlias,
|
||||
$this->orderStatsTable(),
|
||||
$orderStatsAlias,
|
||||
"$customerAlias.customer_id = $orderStatsAlias.customer_id"
|
||||
);
|
||||
|
||||
return $orderStatsAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param array|null $allowedStatuses
|
||||
* @return string - The alias of the joined order stats table
|
||||
*/
|
||||
public function applyOrderStatusFilter(QueryBuilder $queryBuilder, array $allowedStatuses = null): string {
|
||||
if (is_null($allowedStatuses)) {
|
||||
$allowedStatuses = $this->defaultIncludedStatuses();
|
||||
}
|
||||
|
||||
$statusParam = $this->filterHelper->getUniqueParameterName('status');
|
||||
$orderStatsAlias = $this->applyCustomerOrderJoin($queryBuilder);
|
||||
$queryBuilder->andWhere("$orderStatsAlias.status IN (:$statusParam)");
|
||||
$queryBuilder->setParameter($statusParam, $allowedStatuses, ArrayParameterType::STRING);
|
||||
return $orderStatsAlias;
|
||||
}
|
||||
|
||||
private function customerLookupTable(): string {
|
||||
return $this->filterHelper->getPrefixedTable('wc_customer_lookup');
|
||||
}
|
||||
|
||||
private function orderStatsTable(): string {
|
||||
return $this->filterHelper->getPrefixedTable('wc_order_stats');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\ConflictException;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\ORMException;
|
||||
|
||||
class SegmentSaveController {
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var FilterDataMapper */
|
||||
private $filterDataMapper;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
SegmentsRepository $segmentsRepository,
|
||||
FilterDataMapper $filterDataMapper,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->filterDataMapper = $filterDataMapper;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConflictException
|
||||
* @throws NotFoundException
|
||||
* @throws Exceptions\InvalidFilterException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function save(array $data = []): SegmentEntity {
|
||||
$id = isset($data['id']) ? (int)$data['id'] : null;
|
||||
$name = $data['name'] ?? '';
|
||||
|
||||
if (!$this->segmentsRepository->isNameUnique($name, null) && isset($data['force_creation']) && $data['force_creation'] === 'true') {
|
||||
$name = $name . ' (' . wp_generate_password(5, false) . ')';
|
||||
}
|
||||
|
||||
$description = $data['description'] ?? '';
|
||||
$filtersData = $this->filterDataMapper->map($data);
|
||||
|
||||
return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DYNAMIC, $filtersData, $id);
|
||||
}
|
||||
|
||||
public function duplicate(SegmentEntity $segmentEntity): SegmentEntity {
|
||||
$duplicate = clone $segmentEntity;
|
||||
// translators: %s is the name of the segment
|
||||
$duplicate->setName(sprintf(__('Copy of %s', 'mailpoet'), $segmentEntity->getName()));
|
||||
$this->segmentsRepository->verifyNameIsUnique($duplicate->getName(), $duplicate->getId());
|
||||
$this->entityManager->wrapInTransaction(function(EntityManager $entityManager) use ($duplicate, $segmentEntity) {
|
||||
foreach ($segmentEntity->getDynamicFilters() as $dynamicFilter) {
|
||||
$duplicateFilter = clone $dynamicFilter;
|
||||
$duplicate->addDynamicFilter($duplicateFilter);
|
||||
$duplicateFilter->setSegment($duplicate);
|
||||
$entityManager->persist($duplicateFilter);
|
||||
}
|
||||
$entityManager->persist($duplicate);
|
||||
$entityManager->flush();
|
||||
});
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
|
||||
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\Common\Collections\Collection;
|
||||
|
||||
class SegmentDependencyValidator {
|
||||
private const MAILPOET_PREMIUM_PLUGIN = [
|
||||
'id' => 'mailpoet-premium/mailpoet-premium.php',
|
||||
'name' => 'MailPoet Premium',
|
||||
];
|
||||
|
||||
private const WOOCOMMERCE_PLUGIN = [
|
||||
'id' => 'woocommerce/woocommerce.php',
|
||||
'name' => 'WooCommerce',
|
||||
];
|
||||
|
||||
private const WOOCOMMERCE_MEMBERSHIPS_PLUGIN = [
|
||||
'id' => 'woocommerce-memberships/woocommerce-memberships.php',
|
||||
'name' => 'WooCommerce Memberships',
|
||||
];
|
||||
|
||||
private const WOOCOMMERCE_SUBSCRIPTIONS_PLUGIN = [
|
||||
'id' => 'woocommerce-subscriptions/woocommerce-subscriptions.php',
|
||||
'name' => 'WooCommerce Subscriptions',
|
||||
];
|
||||
|
||||
private const REQUIRED_PLUGINS_BY_TYPE = [
|
||||
DynamicSegmentFilterData::TYPE_WOOCOMMERCE => [
|
||||
self::WOOCOMMERCE_PLUGIN,
|
||||
],
|
||||
DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP => [
|
||||
self::WOOCOMMERCE_MEMBERSHIPS_PLUGIN,
|
||||
self::WOOCOMMERCE_PLUGIN,
|
||||
],
|
||||
DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION => [
|
||||
self::WOOCOMMERCE_SUBSCRIPTIONS_PLUGIN,
|
||||
self::WOOCOMMERCE_PLUGIN,
|
||||
],
|
||||
];
|
||||
|
||||
private const REQUIRED_PLUGINS_BY_TYPE_AND_ACTION = [
|
||||
DynamicSegmentFilterData::TYPE_USER_ROLE => [
|
||||
SubscriberTag::TYPE => [
|
||||
self::MAILPOET_PREMIUM_PLUGIN,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** @var SubscribersFeature */
|
||||
private $subscribersFeature;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
SubscribersFeature $subscribersFeature,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->subscribersFeature = $subscribersFeature;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMissingPluginsBySegment(SegmentEntity $segment): array {
|
||||
$dynamicFilters = $segment->getDynamicFilters();
|
||||
$missingPluginNames = $this->getMissingPluginsByAllFilters($dynamicFilters);
|
||||
foreach ($dynamicFilters as $dynamicFilter) {
|
||||
$missingPlugins = $this->getMissingPluginsByFilter($dynamicFilter);
|
||||
if (!$missingPlugins) {
|
||||
continue;
|
||||
}
|
||||
foreach ($missingPlugins as $plugin) {
|
||||
$missingPluginNames[] = $plugin['name'];
|
||||
}
|
||||
}
|
||||
return array_unique($missingPluginNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, DynamicSegmentFilterEntity> $dynamicFilters
|
||||
*/
|
||||
public function getMissingPluginsByAllFilters(Collection $dynamicFilters): array {
|
||||
$missingPluginNames = [];
|
||||
if (
|
||||
count($dynamicFilters) > 1
|
||||
&& (!$this->wp->isPluginActive(self::MAILPOET_PREMIUM_PLUGIN['id'])
|
||||
|| !$this->subscribersFeature->hasValidPremiumKey()
|
||||
|| $this->subscribersFeature->check())
|
||||
) {
|
||||
$missingPluginNames[] = self::MAILPOET_PREMIUM_PLUGIN['name'];
|
||||
}
|
||||
return $missingPluginNames;
|
||||
}
|
||||
|
||||
public function getMissingPluginsByFilter(DynamicSegmentFilterEntity $dynamicSegmentFilter): array {
|
||||
$config = $this->getRequiredPluginsConfig(
|
||||
$dynamicSegmentFilter->getFilterData()->getFilterType() ?? '',
|
||||
$dynamicSegmentFilter->getFilterData()->getAction()
|
||||
);
|
||||
return $this->getMissingPlugins($config);
|
||||
}
|
||||
|
||||
public function canUseDynamicFilterType(string $type): bool {
|
||||
$config = $this->getRequiredPluginsConfig($type);
|
||||
return empty($this->getMissingPlugins($config));
|
||||
}
|
||||
|
||||
private function getRequiredPluginsConfig(string $type, ?string $action = null): array {
|
||||
$requiredPlugins = [];
|
||||
if (isset(self::REQUIRED_PLUGINS_BY_TYPE[$type])) {
|
||||
$requiredPlugins = self::REQUIRED_PLUGINS_BY_TYPE[$type];
|
||||
}
|
||||
if (isset(self::REQUIRED_PLUGINS_BY_TYPE_AND_ACTION[$type][$action])) {
|
||||
$requiredPlugins = array_merge($requiredPlugins, self::REQUIRED_PLUGINS_BY_TYPE_AND_ACTION[$type][$action]);
|
||||
}
|
||||
return $requiredPlugins;
|
||||
}
|
||||
|
||||
private function getMissingPlugins(array $config): array {
|
||||
$missingPlugins = [];
|
||||
foreach ($config as $requiredPlugin) {
|
||||
if (isset($requiredPlugin['id']) && !$this->wp->isPluginActive($requiredPlugin['id'])) {
|
||||
$missingPlugins[] = $requiredPlugin;
|
||||
}
|
||||
}
|
||||
return $missingPlugins;
|
||||
}
|
||||
|
||||
public function getCustomErrorMessage($missingPlugin) {
|
||||
if (
|
||||
$missingPlugin === self::MAILPOET_PREMIUM_PLUGIN['name']
|
||||
&& $this->wp->isPluginActive(self::MAILPOET_PREMIUM_PLUGIN['id'])
|
||||
&& (!$this->subscribersFeature->hasValidPremiumKey() || $this->subscribersFeature->check())
|
||||
) {
|
||||
return [
|
||||
'message' => __('Your current MailPoet plan does not support advanced segments. Please [link]upgrade to a MailPoet Premium plan[/link] to reactivate this segment.', 'mailpoet'),
|
||||
'link' => 'https://account.mailpoet.com',
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Listing\ListingDefinition;
|
||||
use MailPoet\Listing\ListingRepository;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class SegmentListingRepository extends ListingRepository {
|
||||
const DEFAULT_SORT_BY = 'name';
|
||||
|
||||
/** @var WooCommerce */
|
||||
private $wooCommerce;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
WooCommerce $wooCommerce
|
||||
) {
|
||||
parent::__construct($entityManager);
|
||||
$this->wooCommerce = $wooCommerce;
|
||||
}
|
||||
|
||||
protected function applySelectClause(QueryBuilder $queryBuilder) {
|
||||
$queryBuilder->select("PARTIAL s.{id,name,type,description,createdAt,updatedAt,deletedAt,averageEngagementScore}");
|
||||
}
|
||||
|
||||
protected function applyFromClause(QueryBuilder $queryBuilder) {
|
||||
$queryBuilder->from(SegmentEntity::class, 's');
|
||||
}
|
||||
|
||||
protected function applyGroup(QueryBuilder $queryBuilder, string $group) {
|
||||
if ($group === 'trash') {
|
||||
$queryBuilder->andWhere('s.deletedAt IS NOT NULL');
|
||||
} else {
|
||||
$queryBuilder->andWhere('s.deletedAt IS NULL');
|
||||
}
|
||||
}
|
||||
|
||||
protected function applySearch(QueryBuilder $queryBuilder, string $search, array $parameters = []) {
|
||||
$search = Helpers::escapeSearch($search);
|
||||
$queryBuilder
|
||||
->andWhere('s.name LIKE :search or s.description LIKE :search')
|
||||
->setParameter('search', "%$search%");
|
||||
}
|
||||
|
||||
protected function applyFilters(QueryBuilder $queryBuilder, array $filters) {
|
||||
}
|
||||
|
||||
protected function applyParameters(QueryBuilder $queryBuilder, array $parameters) {
|
||||
$types = [SegmentEntity::TYPE_DEFAULT, SegmentEntity::TYPE_WP_USERS];
|
||||
if ($this->wooCommerce->shouldShowWooCommerceSegment()) {
|
||||
$types[] = SegmentEntity::TYPE_WC_USERS;
|
||||
}
|
||||
$queryBuilder
|
||||
->andWhere('s.type IN (:type)')
|
||||
->setParameter('type', $types);
|
||||
}
|
||||
|
||||
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);
|
||||
$this->applyParameters($queryBuilder, $definition->getParameters());
|
||||
|
||||
$queryBuilder->select('count(s.id)');
|
||||
|
||||
if (!$this->wooCommerce->shouldShowWooCommerceSegment()) {
|
||||
$queryBuilder
|
||||
->andWhere('s.type != :wcUsers')
|
||||
->setParameter('wcUsers', SegmentEntity::TYPE_WC_USERS);
|
||||
}
|
||||
|
||||
$allQueryBuilder = clone $queryBuilder;
|
||||
$trashedQueryBuilder = clone $queryBuilder;
|
||||
|
||||
$allQueryBuilder->andWhere('s.deletedAt IS NULL');
|
||||
$allCount = (int)$allQueryBuilder->getQuery()->getSingleScalarResult();
|
||||
|
||||
$trashedQueryBuilder->andWhere('s.deletedAt IS NOT NULL');
|
||||
$trashedCount = (int)$trashedQueryBuilder->getQuery()->getSingleScalarResult();
|
||||
|
||||
return [
|
||||
[
|
||||
'name' => 'all',
|
||||
'label' => __('All', 'mailpoet'),
|
||||
'count' => $allCount,
|
||||
],
|
||||
[
|
||||
'name' => 'trash',
|
||||
'label' => __('Trash', 'mailpoet'),
|
||||
'count' => $trashedCount,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\ConflictException;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\ORMException;
|
||||
|
||||
class SegmentSaveController {
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
SegmentsRepository $segmentsRepository,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConflictException
|
||||
* @throws NotFoundException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function save(array $data = []): SegmentEntity {
|
||||
$id = isset($data['id']) ? (int)$data['id'] : null;
|
||||
$name = $data['name'] ?? '';
|
||||
$description = $data['description'] ?? '';
|
||||
$displayInManageSubPage = isset($data['showInManageSubscriptionPage']) ? (int)$data['showInManageSubscriptionPage'] : false;
|
||||
|
||||
return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DEFAULT, [], $id, (bool)$displayInManageSubPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConflictException
|
||||
*/
|
||||
public function duplicate(SegmentEntity $segmentEntity): SegmentEntity {
|
||||
$duplicate = clone $segmentEntity;
|
||||
// translators: %s is the name of the segment
|
||||
$duplicate->setName(sprintf(__('Copy of %s', 'mailpoet'), $segmentEntity->getName()));
|
||||
|
||||
$this->segmentsRepository->verifyNameIsUnique($duplicate->getName(), $duplicate->getId());
|
||||
|
||||
$this->entityManager->transactional(function (EntityManager $entityManager) use ($duplicate, $segmentEntity) {
|
||||
$entityManager->persist($duplicate);
|
||||
$entityManager->flush();
|
||||
|
||||
$subscriberSegmentTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$conn = $this->entityManager->getConnection();
|
||||
$stmt = $conn->prepare("
|
||||
INSERT INTO $subscriberSegmentTable (segment_id, subscriber_id, status, created_at)
|
||||
SELECT :duplicateId, subscriber_id, status, NOW()
|
||||
FROM $subscriberSegmentTable
|
||||
WHERE segment_id = :segmentId
|
||||
");
|
||||
$stmt->bindValue('duplicateId', $duplicate->getId());
|
||||
$stmt->bindValue('segmentId', $segmentEntity->getId());
|
||||
$stmt->executeQuery();
|
||||
});
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\InvalidStateException;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Segments\DynamicSegments\FilterHandler;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\DBAL\Result;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\Query\Expr\Join;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder as ORMQueryBuilder;
|
||||
|
||||
class SegmentSubscribersRepository {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHandler */
|
||||
private $filterHandler;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHandler $filterHandler,
|
||||
SegmentsRepository $segmentsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHandler = $filterHandler;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
}
|
||||
|
||||
public function findSubscribersIdsInSegment(int $segmentId, array $candidateIds = null): array {
|
||||
return $this->loadSubscriberIdsInSegment($segmentId, $candidateIds);
|
||||
}
|
||||
|
||||
public function getSubscriberIdsInSegment(int $segmentId): array {
|
||||
return $this->loadSubscriberIdsInSegment($segmentId);
|
||||
}
|
||||
|
||||
public function getSubscribersCount(int $segmentId, string $status = null): int {
|
||||
$segment = $this->getSegment($segmentId);
|
||||
$result = $this->getSubscribersStatisticsCount($segment);
|
||||
return (int)$result[$status ?: 'all'];
|
||||
}
|
||||
|
||||
public function getSubscribersCountBySegmentIds(array $segmentIds, string $status = null, ?int $filterSegmentId = null): int {
|
||||
$segmentRepository = $this->entityManager->getRepository(SegmentEntity::class);
|
||||
$segments = $segmentRepository->findBy(['id' => $segmentIds]);
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$queryBuilder = $this->createCountQueryBuilder();
|
||||
|
||||
$subQueries = [];
|
||||
foreach ($segments as $segment) {
|
||||
$segmentQb = $this->createCountQueryBuilder();
|
||||
$segmentQb->select("{$subscribersTable}.id AS inner_id");
|
||||
|
||||
if ($segment->isStatic()) {
|
||||
$segmentQb = $this->filterSubscribersInStaticSegment($segmentQb, $segment, $status);
|
||||
} else {
|
||||
$segmentQb = $this->filterSubscribersInDynamicSegment($segmentQb, $segment, $status);
|
||||
}
|
||||
|
||||
// inner parameters and types have to be merged to outer queryBuilder
|
||||
$queryBuilder->setParameters(array_merge(
|
||||
$segmentQb->getParameters(),
|
||||
$queryBuilder->getParameters()
|
||||
), array_merge(
|
||||
$segmentQb->getParameterTypes(),
|
||||
$queryBuilder->getParameterTypes()
|
||||
));
|
||||
$subQueries[] = $segmentQb->getSQL();
|
||||
}
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf('(%s)', join(' UNION ', $subQueries)),
|
||||
'inner_subscribers',
|
||||
"inner_subscribers.inner_id = {$subscribersTable}.id"
|
||||
);
|
||||
|
||||
try {
|
||||
if (is_int($filterSegmentId)) {
|
||||
$filterSegment = $this->segmentsRepository->verifyDynamicSegmentExists($filterSegmentId);
|
||||
$filterSegmentQb = $this->createCountQueryBuilder();
|
||||
$filterSegmentQb->select("{$subscribersTable}.id AS filter_segment_subscriber_id");
|
||||
$filterSegmentQb = $this->filterSubscribersInDynamicSegment($filterSegmentQb, $filterSegment, $status);
|
||||
$queryBuilder->setParameters(array_merge($filterSegmentQb->getParameters(), $queryBuilder->getParameters()), array_merge($filterSegmentQb->getParameterTypes(), $queryBuilder->getParameterTypes()));
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf('(%s)', $filterSegmentQb->getSQL()),
|
||||
'filter_segment',
|
||||
"filter_segment.filter_segment_subscriber_id = {$subscribersTable}.id"
|
||||
);
|
||||
}
|
||||
} catch (InvalidStateException $exception) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
/** @var string $result */
|
||||
$result = $statement->fetchOne();
|
||||
return (int)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DynamicSegmentFilterData[] $filters
|
||||
* @return int
|
||||
* @throws InvalidStateException
|
||||
*/
|
||||
public function getDynamicSubscribersCount(array $filters): int {
|
||||
$segment = new SegmentEntity('temporary segment', SegmentEntity::TYPE_DYNAMIC, '');
|
||||
foreach ($filters as $filter) {
|
||||
$segment->addDynamicFilter(new DynamicSegmentFilterEntity($segment, $filter));
|
||||
}
|
||||
$queryBuilder = $this->createDynamicStatisticsQueryBuilder();
|
||||
$queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, null);
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
/** @var array{all:string} $result */
|
||||
$result = $statement->fetch();
|
||||
return (int)$result['all'];
|
||||
}
|
||||
|
||||
private function createCountQueryBuilder(): QueryBuilder {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select("count(DISTINCT $subscribersTable.id)")
|
||||
->from($subscribersTable);
|
||||
}
|
||||
|
||||
private function createDynamicStatisticsQueryBuilder(): QueryBuilder {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscribersTable)
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as `all`")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.deleted_at IS NOT NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as trash")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.status = :status_subscribed AND $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_subscribed")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.status = :status_unsubscribed AND $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unsubscribed")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.status = :status_inactive AND $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_inactive")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.status = :status_unconfirmed AND $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unconfirmed")
|
||||
->addSelect("IFNULL(SUM(
|
||||
CASE WHEN $subscribersTable.status = :status_bounced AND $subscribersTable.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_bounced")
|
||||
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
||||
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
||||
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
|
||||
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
|
||||
}
|
||||
|
||||
private function createStaticStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscriberSegmentTable, 'subscriber_segment')
|
||||
->where('subscriber_segment.segment_id = :segment_id')
|
||||
->setParameter('segment_id', $segment->getId())
|
||||
->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as `all`')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.deleted_at IS NOT NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as trash')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_subscribed AND subscriber_segment.status = :status_subscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_subscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN (subscribers.status = :status_unsubscribed OR subscriber_segment.status = :status_unsubscribed) AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unsubscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_inactive AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_inactive')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_unconfirmed AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unconfirmed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_bounced AND subscriber_segment.status != :status_unsubscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_bounced')
|
||||
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
||||
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
||||
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
|
||||
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
|
||||
}
|
||||
|
||||
private function createStaticGlobalStatusStatisticsQueryBuilder(SegmentEntity $segment): QueryBuilder {
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscriberSegmentTable, 'subscriber_segment')
|
||||
->where('subscriber_segment.segment_id = :segment_id')
|
||||
->setParameter('segment_id', $segment->getId())
|
||||
->join('subscriber_segment', $subscribersTable, 'subscribers', 'subscribers.id = subscriber_segment.subscriber_id')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as `all`')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.deleted_at IS NOT NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as trash')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_subscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_subscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_unsubscribed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unsubscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_inactive AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_inactive')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_unconfirmed AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unconfirmed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN subscribers.status = :status_bounced AND subscribers.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_bounced')
|
||||
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
||||
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
||||
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
|
||||
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
|
||||
}
|
||||
|
||||
public function getSubscribersWithoutSegmentCount(): int {
|
||||
$queryBuilder = $this->entityManager->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->select('COUNT(DISTINCT s) AS subscribersCount')
|
||||
->from(SubscriberEntity::class, 's');
|
||||
$this->addConstraintsForSubscribersWithoutSegment($queryBuilder);
|
||||
return (int)$queryBuilder->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function getSubscribersWithoutSegmentStatisticsCount(): array {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$queryBuilder = $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder();
|
||||
$queryBuilder
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as `all`')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.deleted_at IS NOT NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as trash')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.status = :status_subscribed AND s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_subscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.status = :status_unsubscribed AND s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unsubscribed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.status = :status_inactive AND s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_inactive')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.status = :status_unconfirmed AND s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_unconfirmed')
|
||||
->addSelect('IFNULL(SUM(
|
||||
CASE WHEN s.status = :status_bounced AND s.deleted_at IS NULL
|
||||
THEN 1 ELSE 0 END
|
||||
), 0) as :status_bounced')
|
||||
->from($subscribersTable, 's')
|
||||
->setParameter('status_subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
|
||||
->setParameter('status_unsubscribed', SubscriberEntity::STATUS_UNSUBSCRIBED)
|
||||
->setParameter('status_inactive', SubscriberEntity::STATUS_INACTIVE)
|
||||
->setParameter('status_unconfirmed', SubscriberEntity::STATUS_UNCONFIRMED)
|
||||
->setParameter('status_bounced', SubscriberEntity::STATUS_BOUNCED);
|
||||
|
||||
$this->addConstraintsForSubscribersWithoutSegmentToDBAL($queryBuilder);
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
$result = $statement->fetch();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function addConstraintsForSubscribersWithoutSegment(ORMQueryBuilder $queryBuilder): void {
|
||||
$deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
|
||||
$deletedSegmentsQueryBuilder->select('sg.id')
|
||||
->from(SegmentEntity::class, 'sg')
|
||||
->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
|
||||
|
||||
$queryBuilder
|
||||
->leftJoin(
|
||||
's.subscriberSegments',
|
||||
'ssg',
|
||||
Join::WITH,
|
||||
(string)$queryBuilder->expr()->andX(
|
||||
$queryBuilder->expr()->eq('ssg.subscriber', 's.id'),
|
||||
$queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
|
||||
$queryBuilder->expr()->notIn('ssg.segment', $deletedSegmentsQueryBuilder->getDQL())
|
||||
)
|
||||
)
|
||||
->andWhere('ssg.id IS NULL')
|
||||
->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
}
|
||||
|
||||
public function addConstraintsForSubscribersWithoutSegmentToDBAL(QueryBuilder $queryBuilder): void {
|
||||
$deletedSegmentsQueryBuilder = $this->entityManager->createQueryBuilder();
|
||||
$subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$deletedSegmentsQueryBuilder->select('sg.id')
|
||||
->from(SegmentEntity::class, 'sg')
|
||||
->where($deletedSegmentsQueryBuilder->expr()->isNotNull('sg.deletedAt'));
|
||||
|
||||
$queryBuilder
|
||||
->leftJoin(
|
||||
's',
|
||||
$subscribersSegmentTable,
|
||||
'ssg',
|
||||
(string)$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->eq('ssg.subscriber_id', 's.id'),
|
||||
$queryBuilder->expr()->eq('ssg.status', ':statusSubscribed'),
|
||||
$queryBuilder->expr()->notIn('ssg.segment_id', $deletedSegmentsQueryBuilder->getQuery()->getSQL())
|
||||
)
|
||||
)
|
||||
->andWhere('ssg.id IS NULL')
|
||||
->setParameter('statusSubscribed', SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
}
|
||||
|
||||
private function loadSubscriberIdsInSegment(int $segmentId, array $candidateIds = null): array {
|
||||
$segment = $this->getSegment($segmentId);
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$queryBuilder = $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select("DISTINCT $subscribersTable.id")
|
||||
->from($subscribersTable);
|
||||
|
||||
if ($segment->isStatic()) {
|
||||
$queryBuilder = $this->filterSubscribersInStaticSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
} else {
|
||||
$queryBuilder = $this->filterSubscribersInDynamicSegment($queryBuilder, $segment, SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
}
|
||||
|
||||
if ($candidateIds) {
|
||||
$queryBuilder->andWhere("$subscribersTable.id IN (:candidateIds)")
|
||||
->setParameter('candidateIds', $candidateIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
$result = $statement->fetchAll();
|
||||
return array_column($result, 'id');
|
||||
}
|
||||
|
||||
private function filterSubscribersInStaticSegment(
|
||||
QueryBuilder $queryBuilder,
|
||||
SegmentEntity $segment,
|
||||
string $status = null
|
||||
): QueryBuilder {
|
||||
$subscribersSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$parameterName = "segment_{$segment->getId()}"; // When we use this method more times the parameter name has to be unique
|
||||
$queryBuilder = $queryBuilder->join(
|
||||
$subscribersTable,
|
||||
$subscribersSegmentsTable,
|
||||
'subsegment',
|
||||
"subsegment.subscriber_id = $subscribersTable.id AND subsegment.segment_id = :$parameterName"
|
||||
)->andWhere("$subscribersTable.deleted_at IS NULL")
|
||||
->setParameter($parameterName, $segment->getId());
|
||||
if ($status) {
|
||||
$queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
|
||||
->andWhere("subsegment.status = :status")
|
||||
->setParameter('status', $status);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function filterSubscribersInDynamicSegment(
|
||||
QueryBuilder $queryBuilder,
|
||||
SegmentEntity $segment,
|
||||
string $status = null
|
||||
): QueryBuilder {
|
||||
$filters = [];
|
||||
$dynamicFilters = $segment->getDynamicFilters();
|
||||
foreach ($dynamicFilters as $dynamicFilter) {
|
||||
$filters[] = $dynamicFilter->getFilterData();
|
||||
}
|
||||
|
||||
// We don't allow dynamic segment without filers since it would return all subscribers
|
||||
// For BC compatibility fetching an empty result
|
||||
if (count($filters) === 0) {
|
||||
return $queryBuilder->andWhere('0 = 1');
|
||||
} elseif ($segment instanceof SegmentEntity) {
|
||||
try {
|
||||
$queryBuilder = $this->filterHandler->apply($queryBuilder, $segment);
|
||||
} catch (InvalidFilterException $e) {
|
||||
// If a segment has an invalid filter, we should simply consider it empty instead of throwing
|
||||
// an unhandled error. Unhandled errors here can break many admin pages.
|
||||
$queryBuilder->andWhere('0 = 1');
|
||||
}
|
||||
}
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$queryBuilder = $queryBuilder->andWhere("$subscribersTable.deleted_at IS NULL");
|
||||
if ($status) {
|
||||
$queryBuilder = $queryBuilder->andWhere("$subscribersTable.status = :status")
|
||||
->setParameter('status', $status);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getSegment(int $id): SegmentEntity {
|
||||
$segment = $this->entityManager->find(SegmentEntity::class, $id);
|
||||
if (!$segment instanceof SegmentEntity) {
|
||||
throw new NotFoundException('Segment not found');
|
||||
}
|
||||
return $segment;
|
||||
}
|
||||
|
||||
private function executeQuery(QueryBuilder $queryBuilder): Result {
|
||||
$result = $queryBuilder->execute();
|
||||
// Execute for select always returns statement but PHP Stan doesn't know that :(
|
||||
if (!$result instanceof Result) {
|
||||
throw new InvalidStateException('Invalid query.');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getSubscribersGlobalStatusStatisticsCount(SegmentEntity $segment): array {
|
||||
if ($segment->isStatic()) {
|
||||
$queryBuilder = $this->createStaticGlobalStatusStatisticsQueryBuilder($segment);
|
||||
} else {
|
||||
$queryBuilder = $this->createDynamicStatisticsQueryBuilder();
|
||||
$this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
|
||||
}
|
||||
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
return $statement->fetch();
|
||||
}
|
||||
|
||||
public function getSubscribersStatisticsCount(SegmentEntity $segment): array {
|
||||
if ($segment->isStatic()) {
|
||||
$queryBuilder = $this->createStaticStatisticsQueryBuilder($segment);
|
||||
} else {
|
||||
$queryBuilder = $this->createDynamicStatisticsQueryBuilder();
|
||||
$this->filterSubscribersInDynamicSegment($queryBuilder, $segment);
|
||||
}
|
||||
|
||||
$statement = $this->executeQuery($queryBuilder);
|
||||
return $statement->fetch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\DynamicSegments\FilterHandler;
|
||||
use MailPoetVendor\Doctrine\DBAL\Result;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class SegmentsFinder {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHandler */
|
||||
private $filterHandler;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHandler $filterHandler,
|
||||
SegmentsRepository $segmentsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHandler = $filterHandler;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
}
|
||||
|
||||
/** @return SegmentEntity[] */
|
||||
public function findSegments(SubscriberEntity $subscriber): array {
|
||||
return array_merge(
|
||||
$this->findStaticSegments($subscriber),
|
||||
$this->findDynamicSegments($subscriber)
|
||||
);
|
||||
}
|
||||
|
||||
/** @return SegmentEntity[] */
|
||||
public function findStaticSegments(SubscriberEntity $subscriber): array {
|
||||
return $subscriber->getSegments()->toArray();
|
||||
}
|
||||
|
||||
/** @return SegmentEntity[] */
|
||||
public function findDynamicSegments(SubscriberEntity $subscriber): array {
|
||||
$segments = $this->segmentsRepository->findBy([
|
||||
'type' => SegmentEntity::TYPE_DYNAMIC,
|
||||
]);
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$queryBuilder = $this->entityManager->getConnection()->createQueryBuilder()
|
||||
->select('id')
|
||||
->from($subscribersTable)
|
||||
->where('id = :subscriberId')
|
||||
->setParameter('subscriberId', $subscriber->getId());
|
||||
|
||||
$matchingSegments = [];
|
||||
foreach ($segments as $segment) {
|
||||
$result = $this->filterHandler->apply(clone $queryBuilder, $segment)->execute();
|
||||
if ($result instanceof Result && $result->fetchOne()) {
|
||||
$matchingSegments[] = $segment;
|
||||
}
|
||||
}
|
||||
return $matchingSegments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTime;
|
||||
use MailPoet\ConflictException;
|
||||
use MailPoet\Doctrine\Repository;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\NewsletterSegmentEntity;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\Form\FormsRepository;
|
||||
use MailPoet\InvalidStateException;
|
||||
use MailPoet\Logging\LoggerFactory;
|
||||
use MailPoet\Newsletter\Segment\NewsletterSegmentRepository;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\ORMException;
|
||||
|
||||
/**
|
||||
* @extends Repository<SegmentEntity>
|
||||
*/
|
||||
class SegmentsRepository extends Repository {
|
||||
|
||||
/** @var NewsletterSegmentRepository */
|
||||
private $newsletterSegmentRepository;
|
||||
|
||||
/** @var FormsRepository */
|
||||
private $formsRepository;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var LoggerFactory */
|
||||
private $loggerFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
NewsletterSegmentRepository $newsletterSegmentRepository,
|
||||
FormsRepository $formsRepository,
|
||||
WPFunctions $wp,
|
||||
LoggerFactory $loggerFactory
|
||||
) {
|
||||
parent::__construct($entityManager);
|
||||
$this->newsletterSegmentRepository = $newsletterSegmentRepository;
|
||||
$this->formsRepository = $formsRepository;
|
||||
$this->wp = $wp;
|
||||
$this->loggerFactory = $loggerFactory;
|
||||
}
|
||||
|
||||
protected function getEntityClassName() {
|
||||
return SegmentEntity::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $types
|
||||
* @return SegmentEntity[]
|
||||
*/
|
||||
public function findByTypeNotIn(array $types): array {
|
||||
return $this->doctrineRepository->createQueryBuilder('s')
|
||||
->select('s')
|
||||
->where('s.type NOT IN (:types)')
|
||||
->setParameter('types', $types)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function getWPUsersSegment(): SegmentEntity {
|
||||
$cached = current(
|
||||
array_filter(
|
||||
$this->getAllFromIdentityMap(),
|
||||
fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WP_USERS
|
||||
)
|
||||
);
|
||||
|
||||
if ($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WP_USERS]);
|
||||
if (!$segment) {
|
||||
// create the wp users segment
|
||||
$segment = new SegmentEntity(
|
||||
__('WordPress Users', 'mailpoet'),
|
||||
SegmentEntity::TYPE_WP_USERS,
|
||||
__('This list contains all of your WordPress users.', 'mailpoet')
|
||||
);
|
||||
$this->entityManager->persist($segment);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
return $segment;
|
||||
}
|
||||
|
||||
public function getWooCommerceSegment(): SegmentEntity {
|
||||
$cached = current(
|
||||
array_filter(
|
||||
$this->getAllFromIdentityMap(),
|
||||
fn(SegmentEntity $segment) => $segment->getType() === SegmentEntity::TYPE_WC_USERS
|
||||
)
|
||||
);
|
||||
|
||||
if ($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$segment = $this->findOneBy(['type' => SegmentEntity::TYPE_WC_USERS]);
|
||||
if (!$segment) {
|
||||
// create the WooCommerce customers segment
|
||||
$segment = new SegmentEntity(
|
||||
__('WooCommerce Customers', 'mailpoet'),
|
||||
SegmentEntity::TYPE_WC_USERS,
|
||||
__('This list contains all of your WooCommerce customers.', 'mailpoet')
|
||||
);
|
||||
$this->entityManager->persist($segment);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
return $segment;
|
||||
}
|
||||
|
||||
public function getCountsPerType(): array {
|
||||
$results = $this->doctrineRepository->createQueryBuilder('s')
|
||||
->select('s.type, COUNT(s) as cnt')
|
||||
->where('s.deletedAt IS NULL')
|
||||
->groupBy('s.type')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$countMap = [];
|
||||
foreach ($results as $result) {
|
||||
$countMap[$result['type']] = (int)$result['cnt'];
|
||||
}
|
||||
return $countMap;
|
||||
}
|
||||
|
||||
public function isNameUnique(string $name, ?int $id): bool {
|
||||
$qb = $this->doctrineRepository->createQueryBuilder('s')
|
||||
->select('s')
|
||||
->where('s.name = :name')
|
||||
->setParameter('name', $name);
|
||||
|
||||
if ($id !== null) {
|
||||
$qb->andWhere('s.id != :id')
|
||||
->setParameter('id', $id);
|
||||
}
|
||||
|
||||
$results = $qb->getQuery()
|
||||
->getResult();
|
||||
|
||||
return count($results) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConflictException
|
||||
*/
|
||||
public function verifyNameIsUnique(string $name, ?int $id): void {
|
||||
if (!$this->isNameUnique($name, $id)) {
|
||||
throw new ConflictException("Could not create new segment with name [{$name}] because a segment with that name already exists.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*
|
||||
* @return SegmentEntity
|
||||
* @throws InvalidStateException
|
||||
*/
|
||||
public function verifyDynamicSegmentExists(int $id): SegmentEntity {
|
||||
try {
|
||||
$dynamicSegment = $this->findOneById($id);
|
||||
if (!$dynamicSegment instanceof SegmentEntity) {
|
||||
throw InvalidStateException::create()->withMessage(sprintf("Could not find segment with ID '%s'.", $id));
|
||||
}
|
||||
if ($dynamicSegment->getType() !== SegmentEntity::TYPE_DYNAMIC) {
|
||||
throw InvalidStateException::create()->withMessage(sprintf("Segment with ID '%s' is not a dynamic segment. Its type is %s.", $id, $dynamicSegment->getType()));
|
||||
}
|
||||
} catch (InvalidStateException $exception) {
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_SEGMENTS)->error(sprintf("Could not verify existence of dynamic segment: %s", $exception->getMessage()));
|
||||
throw $exception;
|
||||
}
|
||||
return $dynamicSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DynamicSegmentFilterData[] $filtersData
|
||||
* @throws ConflictException
|
||||
* @throws NotFoundException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function createOrUpdate(
|
||||
string $name,
|
||||
string $description = '',
|
||||
string $type = SegmentEntity::TYPE_DEFAULT,
|
||||
array $filtersData = [],
|
||||
?int $id = null,
|
||||
bool $displayInManageSubscriptionPage = true
|
||||
): SegmentEntity {
|
||||
$displayInManageSubPage = $type === SegmentEntity::TYPE_DEFAULT ? $displayInManageSubscriptionPage : false;
|
||||
|
||||
if ($id) {
|
||||
$segment = $this->findOneById($id);
|
||||
if (!$segment instanceof SegmentEntity) {
|
||||
throw new NotFoundException("Segment with ID [{$id}] was not found.");
|
||||
}
|
||||
if ($name !== $segment->getName()) {
|
||||
$this->verifyNameIsUnique($name, $id);
|
||||
$segment->setName($name);
|
||||
}
|
||||
$segment->setDescription($description);
|
||||
$segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
|
||||
} else {
|
||||
$this->verifyNameIsUnique($name, $id);
|
||||
$segment = new SegmentEntity($name, $type, $description);
|
||||
$segment->setDisplayInManageSubscriptionPage($displayInManageSubPage);
|
||||
$this->persist($segment);
|
||||
}
|
||||
|
||||
// We want to remove redundant filters before update
|
||||
while ($segment->getDynamicFilters()->count() > count($filtersData)) {
|
||||
$filterEntity = $segment->getDynamicFilters()->last();
|
||||
if ($filterEntity) {
|
||||
$segment->getDynamicFilters()->removeElement($filterEntity);
|
||||
$this->entityManager->remove($filterEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$createOrUpdateFilter = function ($filterData, $key) use ($segment) {
|
||||
if ($filterData instanceof DynamicSegmentFilterData) {
|
||||
$filterEntity = $segment->getDynamicFilters()->get($key);
|
||||
if (!$filterEntity instanceof DynamicSegmentFilterEntity) {
|
||||
$filterEntity = new DynamicSegmentFilterEntity($segment, $filterData);
|
||||
$segment->getDynamicFilters()->add($filterEntity);
|
||||
$this->entityManager->persist($filterEntity);
|
||||
} else {
|
||||
$filterEntity->setFilterData($filterData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$wpActionName = 'mailpoet_dynamic_segments_filters_save';
|
||||
if ($this->wp->hasAction($wpActionName)) {
|
||||
$this->wp->doAction($wpActionName, $createOrUpdateFilter, $filtersData);
|
||||
} else {
|
||||
$filterData = reset($filtersData);
|
||||
$key = key($filtersData);
|
||||
$createOrUpdateFilter($filterData, $key);
|
||||
}
|
||||
|
||||
$this->flush();
|
||||
return $segment;
|
||||
}
|
||||
|
||||
public function bulkDelete(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
||||
if (empty($ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$this->entityManager->transactional(function (EntityManager $entityManager) use ($ids, $type, &$count) {
|
||||
$subscriberSegmentTable = $entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$segmentTable = $entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
||||
$segmentFiltersTable = $entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
|
||||
|
||||
$entityManager->getConnection()->executeStatement("
|
||||
DELETE ss FROM $subscriberSegmentTable ss
|
||||
JOIN $segmentTable s ON ss.`segment_id` = s.`id`
|
||||
WHERE ss.`segment_id` IN (:ids)
|
||||
AND s.`type` = :type
|
||||
", [
|
||||
'ids' => $ids,
|
||||
'type' => $type,
|
||||
], ['ids' => ArrayParameterType::INTEGER]);
|
||||
|
||||
$entityManager->getConnection()->executeStatement("
|
||||
DELETE df FROM $segmentFiltersTable df
|
||||
WHERE df.`segment_id` IN (:ids)
|
||||
", [
|
||||
'ids' => $ids,
|
||||
], ['ids' => ArrayParameterType::INTEGER]);
|
||||
|
||||
$queryBuilder = $entityManager->createQueryBuilder();
|
||||
$count = $queryBuilder->delete(SegmentEntity::class, 's')
|
||||
->where('s.id IN (:ids)')
|
||||
->andWhere('s.type = :type')
|
||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER)
|
||||
->setParameter('type', $type, ParameterType::STRING)
|
||||
->getQuery()->execute();
|
||||
|
||||
$queryBuilder = $entityManager->createQueryBuilder();
|
||||
$queryBuilder->delete(NewsletterSegmentEntity::class, 'ns')
|
||||
->where('ns.segment IN (:ids)')
|
||||
->setParameter('ids', $ids, ArrayParameterType::INTEGER)
|
||||
->getQuery()->execute();
|
||||
});
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function bulkTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
||||
$activelyUsedInNewsletters = $this->newsletterSegmentRepository->getSubjectsOfActivelyUsedEmailsForSegments($ids);
|
||||
$activelyUsedInForms = $this->formsRepository->getNamesOfFormsForSegments();
|
||||
$activelyUsed = array_unique(array_merge(array_keys($activelyUsedInNewsletters), array_keys($activelyUsedInForms)));
|
||||
$ids = array_diff($ids, $activelyUsed);
|
||||
return $this->updateDeletedAt($ids, new Carbon(), $type);
|
||||
}
|
||||
|
||||
public function doTrash(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
||||
return $this->updateDeletedAt($ids, new Carbon(), $type);
|
||||
}
|
||||
|
||||
public function bulkRestore(array $ids, string $type = SegmentEntity::TYPE_DEFAULT): int {
|
||||
return $this->updateDeletedAt($ids, null, $type);
|
||||
}
|
||||
|
||||
private function updateDeletedAt(array $ids, ?DateTime $deletedAt, string $type): int {
|
||||
if (empty($ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rows = $this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
|
||||
->set('s.deletedAt', ':deletedAt')
|
||||
->where('s.id IN (:ids)')
|
||||
->andWhere('s.type IN (:type)')
|
||||
->setParameter('deletedAt', $deletedAt)
|
||||
->setParameter('ids', $ids)
|
||||
->setParameter('type', $type)
|
||||
->getQuery()->execute();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function findByUpdatedScoreNotInLastDay(int $limit): array {
|
||||
$dateTime = (new Carbon())->subDay();
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('s')
|
||||
->from(SegmentEntity::class, 's')
|
||||
->where('s.averageEngagementScoreUpdatedAt IS NULL')
|
||||
->orWhere('s.averageEngagementScoreUpdatedAt < :dateTime')
|
||||
->setParameter('dateTime', $dateTime)
|
||||
->getQuery()
|
||||
->setMaxResults($limit)
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns count of segments that have more than one dynamic filter
|
||||
*/
|
||||
public function getSegmentCountWithMultipleFilters(): int {
|
||||
$segmentFiltersTable = $this->entityManager->getClassMetadata(DynamicSegmentFilterEntity::class)->getTableName();
|
||||
$qbInner = $this->entityManager->getConnection()->createQueryBuilder()
|
||||
->select('COUNT(DISTINCT sf.id) AS segmentCount')
|
||||
->from($segmentFiltersTable, 'sf')
|
||||
->groupBy('sf.segment_id')
|
||||
->having('COUNT(sf.id) > 1');
|
||||
/** @var null|int $result */
|
||||
$result = $this->entityManager->getConnection()->createQueryBuilder()
|
||||
->select('count(*)')
|
||||
->from(sprintf('(%s) as subCounts', $qbInner->getSQL()))
|
||||
->execute()
|
||||
->fetchOne();
|
||||
return (int)$result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Subscribers\SubscribersCountsController;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Result;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class SegmentsSimpleListRepository {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var SubscribersCountsController */
|
||||
private $subscribersCountsController;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
SubscribersCountsController $subscribersCountsController
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->subscribersCountsController = $subscribersCountsController;
|
||||
}
|
||||
|
||||
/**
|
||||
* This fetches list of all segments basic data and count of subscribed subscribers.
|
||||
* @return array<array{id: string, name: string, type: string, subscribers: int}>
|
||||
*/
|
||||
public function getListWithSubscribedSubscribersCounts(array $segmentTypes = []): array {
|
||||
return $this->getList(
|
||||
$segmentTypes,
|
||||
SubscriberEntity::STATUS_SUBSCRIBED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This fetches list of all segments basic data and count of subscribers associated to a segment regardless their subscription status.
|
||||
* @return array<array{id: string, name: string, type: string, subscribers: int}>
|
||||
*/
|
||||
public function getListWithAssociatedSubscribersCounts(array $segmentTypes = []): array {
|
||||
return $this->getList(
|
||||
$segmentTypes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a virtual segment with for subscribers without list
|
||||
* @return array<array{id: string, name: string, type: string, subscribers: int}>
|
||||
*/
|
||||
public function addVirtualSubscribersWithoutListSegment(array $segments): array {
|
||||
$withoutSegmentStats = $this->subscribersCountsController->getSubscribersWithoutSegmentStatisticsCount();
|
||||
$segments[] = [
|
||||
'id' => '0',
|
||||
'type' => SegmentEntity::TYPE_WITHOUT_LIST,
|
||||
'name' => __('Subscribers without a list', 'mailpoet'),
|
||||
'subscribers' => $withoutSegmentStats['all'],
|
||||
];
|
||||
return $segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{id: string, name: string, type: string, subscribers: int}>
|
||||
*/
|
||||
private function getList(
|
||||
array $segmentTypes = [],
|
||||
string $subscriberGlobalStatus = null
|
||||
): array {
|
||||
$segmentsTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName();
|
||||
|
||||
$segmentsDataQuery = $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder();
|
||||
|
||||
$segmentsDataQuery->select(
|
||||
"segments.id, segments.name, segments.type"
|
||||
)->from($segmentsTable, 'segments')
|
||||
->where('segments.deleted_at IS NULL')
|
||||
->orderBy('segments.name');
|
||||
|
||||
if (!empty($segmentTypes)) {
|
||||
$segmentsDataQuery
|
||||
->andWhere('segments.type IN (:typesParam)')
|
||||
->setParameter('typesParam', $segmentTypes, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
$result = $segmentsDataQuery->executeQuery();
|
||||
if (!$result instanceof Result) {
|
||||
return [];
|
||||
}
|
||||
$segments = $result->fetchAll();
|
||||
|
||||
// Fetch subscribers counts for static and dynamic segments and correct data types
|
||||
foreach ($segments as $key => $segment) {
|
||||
// BC compatibility fix. PHP8.1+ returns integer but JS apps expect string
|
||||
$segments[$key]['id'] = (string)$segment['id'];
|
||||
$statisticsKey = $subscriberGlobalStatus ?: 'all';
|
||||
$segments[$key]['subscribers'] = (int)$this->subscribersCountsController->getSegmentStatisticsCountById($segment['id'])[$statisticsKey];
|
||||
}
|
||||
/* @var array<array{id: string, name: string, type: string, subscribers: int}> */
|
||||
return $segments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\ScheduledTaskEntity;
|
||||
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\InvalidStateException;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class SubscribersFinder {
|
||||
|
||||
/** @var SegmentSubscribersRepository */
|
||||
private $segmentSubscriberRepository;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
SegmentSubscribersRepository $segmentSubscriberRepository,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->segmentSubscriberRepository = $segmentSubscriberRepository;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws InvalidStateException
|
||||
*/
|
||||
public function findSubscribersInSegments($subscribersToProcessIds, $newsletterSegmentsIds, ?int $filterSegmentId = null) {
|
||||
$result = [];
|
||||
foreach ($newsletterSegmentsIds as $segmentId) {
|
||||
$segment = $this->segmentsRepository->findOneById($segmentId);
|
||||
if (!$segment instanceof SegmentEntity) {
|
||||
continue; // skip deleted segments
|
||||
}
|
||||
$result = array_merge($result, $this->findSubscribersInSegment($segment, $subscribersToProcessIds));
|
||||
}
|
||||
|
||||
if (is_int($filterSegmentId)) {
|
||||
$filterSegment = $this->segmentsRepository->verifyDynamicSegmentExists($filterSegmentId);
|
||||
$idsInFilterSegment = $this->findSubscribersInSegment($filterSegment, $subscribersToProcessIds);
|
||||
$result = array_intersect($result, $idsInFilterSegment);
|
||||
}
|
||||
|
||||
return $this->unique($result);
|
||||
}
|
||||
|
||||
private function findSubscribersInSegment(SegmentEntity $segment, $subscribersToProcessIds): array {
|
||||
try {
|
||||
return $this->segmentSubscriberRepository->findSubscribersIdsInSegment((int)$segment->getId(), $subscribersToProcessIds);
|
||||
} catch (InvalidStateException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScheduledTaskEntity $task
|
||||
* @param array<int> $segmentIds
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addSubscribersToTaskFromSegments(ScheduledTaskEntity $task, array $segmentIds, ?int $filterSegmentId = null): void {
|
||||
// Prepare subscribers on the DB side for performance reasons
|
||||
if (is_int($filterSegmentId)) {
|
||||
try {
|
||||
$this->segmentsRepository->verifyDynamicSegmentExists($filterSegmentId);
|
||||
} catch (InvalidStateException $exception) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$staticSegmentIds = [];
|
||||
$dynamicSegmentIds = [];
|
||||
foreach ($segmentIds as $segment) {
|
||||
$segment = $this->segmentsRepository->findOneById($segment);
|
||||
if ($segment instanceof SegmentEntity) {
|
||||
if ($segment->isStatic()) {
|
||||
$staticSegmentIds[] = (int)$segment->getId();
|
||||
} elseif ($segment->getType() === SegmentEntity::TYPE_DYNAMIC) {
|
||||
$dynamicSegmentIds[] = (int)$segment->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
$count = 0;
|
||||
if (!empty($staticSegmentIds)) {
|
||||
$count += $this->addSubscribersToTaskFromStaticSegments($task, $staticSegmentIds, $filterSegmentId);
|
||||
}
|
||||
if (!empty($dynamicSegmentIds)) {
|
||||
$count += $this->addSubscribersToTaskFromDynamicSegments($task, $dynamicSegmentIds, $filterSegmentId);
|
||||
}
|
||||
if ($count > 0) {
|
||||
$this->entityManager->refresh($task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScheduledTaskEntity $task
|
||||
* @param array<int> $segmentIds
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function addSubscribersToTaskFromStaticSegments(ScheduledTaskEntity $task, array $segmentIds, ?int $filterSegmentId = null) {
|
||||
$scheduledTaskSubscriberTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$selectQueryBuilder = $connection->createQueryBuilder();
|
||||
$selectQueryBuilder
|
||||
->select('DISTINCT :task_id as task_id', 'subscribers.id as subscriber_id', ':processed as processed')
|
||||
->from($subscriberSegmentTable, 'relation')
|
||||
->join('relation', $subscriberTable, 'subscribers', 'subscribers.id = relation.subscriber_id')
|
||||
->where('subscribers.deleted_at IS NULL')
|
||||
->andWhere('subscribers.status = :subscribers_status')
|
||||
->andWhere('relation.status = :relation_status')
|
||||
->andWhere($selectQueryBuilder->expr()->in('relation.segment_id', ':segment_ids'))
|
||||
->setParameter('task_id', $task->getId(), ParameterType::INTEGER)
|
||||
->setParameter('processed', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED, ParameterType::INTEGER)
|
||||
->setParameter('subscribers_status', SubscriberEntity::STATUS_SUBSCRIBED, ParameterType::STRING)
|
||||
->setParameter('relation_status', SubscriberEntity::STATUS_SUBSCRIBED, ParameterType::STRING)
|
||||
->setParameter('segment_ids', $segmentIds, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($filterSegmentId) {
|
||||
$filterSegmentSubscriberIds = $this->segmentSubscriberRepository->findSubscribersIdsInSegment($filterSegmentId);
|
||||
$selectQueryBuilder
|
||||
->andWhere($selectQueryBuilder->expr()->in('subscribers.id', ':filterSegmentSubscriberIds'))
|
||||
->setParameter('filterSegmentSubscriberIds', $filterSegmentSubscriberIds, ArrayParameterType::INTEGER);
|
||||
}
|
||||
|
||||
// queryBuilder doesn't support INSERT IGNORE directly
|
||||
$sql = "INSERT IGNORE INTO $scheduledTaskSubscriberTable (task_id, subscriber_id, processed) " . $selectQueryBuilder->getSQL();
|
||||
$result = $connection->executeQuery($sql, $selectQueryBuilder->getParameters(), $selectQueryBuilder->getParameterTypes());
|
||||
|
||||
return (int)$result->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScheduledTaskEntity $task
|
||||
* @param array<int> $segmentIds
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function addSubscribersToTaskFromDynamicSegments(ScheduledTaskEntity $task, array $segmentIds, ?int $filterSegmentId = null) {
|
||||
$count = 0;
|
||||
foreach ($segmentIds as $segmentId) {
|
||||
$count += $this->addSubscribersToTaskFromDynamicSegment($task, (int)$segmentId, $filterSegmentId);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function addSubscribersToTaskFromDynamicSegment(ScheduledTaskEntity $task, int $segmentId, ?int $filterSegmentId) {
|
||||
$count = 0;
|
||||
$subscriberIds = $this->segmentSubscriberRepository->getSubscriberIdsInSegment($segmentId);
|
||||
|
||||
if ($filterSegmentId) {
|
||||
$filterSegmentSubscriberIds = $this->segmentSubscriberRepository->getSubscriberIdsInSegment($filterSegmentId);
|
||||
$subscriberIds = array_intersect($subscriberIds, $filterSegmentSubscriberIds);
|
||||
}
|
||||
|
||||
if ($subscriberIds) {
|
||||
$count += $this->addSubscribersToTaskByIds($task, $subscriberIds);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function addSubscribersToTaskByIds(ScheduledTaskEntity $task, array $subscriberIds) {
|
||||
$scheduledTaskSubscriberTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
|
||||
$subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
$connection = $this->entityManager->getConnection();
|
||||
|
||||
$result = $connection->executeQuery(
|
||||
"INSERT IGNORE INTO $scheduledTaskSubscriberTable
|
||||
(task_id, subscriber_id, processed)
|
||||
SELECT DISTINCT ? as task_id, subscribers.`id` as subscriber_id, ? as processed
|
||||
FROM $subscriberTable subscribers
|
||||
WHERE subscribers.`deleted_at` IS NULL
|
||||
AND subscribers.`status` = ?
|
||||
AND subscribers.`id` IN (?)",
|
||||
[
|
||||
$task->getId(),
|
||||
ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED,
|
||||
SubscriberEntity::STATUS_SUBSCRIBED,
|
||||
$subscriberIds,
|
||||
],
|
||||
[
|
||||
ParameterType::INTEGER,
|
||||
ParameterType::INTEGER,
|
||||
ParameterType::STRING,
|
||||
ArrayParameterType::INTEGER,
|
||||
]
|
||||
);
|
||||
|
||||
return $result->rowCount();
|
||||
}
|
||||
|
||||
private function unique(array $subscriberIds) {
|
||||
$result = [];
|
||||
foreach ($subscriberIds as $id) {
|
||||
$result[$id] = $id;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\SubscriberChangesNotifier;
|
||||
use MailPoet\DI\ContainerWrapper;
|
||||
use MailPoet\Doctrine\WPDB\Connection;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\Newsletter\Scheduler\WelcomeScheduler;
|
||||
use MailPoet\Services\Validator;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\Subscribers\ConfirmationEmailMailer;
|
||||
use MailPoet\Subscribers\Source;
|
||||
use MailPoet\Subscribers\SubscriberSegmentRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WP {
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WelcomeScheduler */
|
||||
private $welcomeScheduler;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $wooHelper;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var SubscriberChangesNotifier */
|
||||
private $subscriberChangesNotifier;
|
||||
|
||||
private $subscriberSegmentRepository;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var string */
|
||||
private $subscribersTable;
|
||||
|
||||
/** @var \MailPoetVendor\Doctrine\DBAL\Connection */
|
||||
private $databaseConnection;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp,
|
||||
WelcomeScheduler $welcomeScheduler,
|
||||
WooCommerceHelper $wooHelper,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
SubscriberSegmentRepository $subscriberSegmentRepository,
|
||||
SubscriberChangesNotifier $subscriberChangesNotifier,
|
||||
Validator $validator,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->welcomeScheduler = $welcomeScheduler;
|
||||
$this->wooHelper = $wooHelper;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
|
||||
$this->subscriberChangesNotifier = $subscriberChangesNotifier;
|
||||
$this->validator = $validator;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->databaseConnection = $this->entityManager->getConnection();
|
||||
$this->subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $wpUserId
|
||||
* @param array|false $oldWpUserData
|
||||
*/
|
||||
public function synchronizeUser(int $wpUserId, $oldWpUserData = false): void {
|
||||
$wpUser = \get_userdata($wpUserId);
|
||||
if ($wpUser === false) return;
|
||||
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['wpUserId' => $wpUserId]);
|
||||
|
||||
$currentFilter = $this->wp->currentFilter();
|
||||
// Delete
|
||||
if (in_array($currentFilter, ['delete_user', 'deleted_user', 'remove_user_from_blog'])) {
|
||||
if ($subscriber instanceof SubscriberEntity) {
|
||||
$this->deleteSubscriber($subscriber);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->handleCreatingOrUpdatingSubscriber($currentFilter, $wpUser, $subscriber, $oldWpUserData);
|
||||
}
|
||||
|
||||
private function deleteSubscriber(SubscriberEntity $subscriber): void {
|
||||
$this->subscribersRepository->remove($subscriber);
|
||||
$this->subscribersRepository->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currentFilter
|
||||
* @param \WP_User $wpUser
|
||||
* @param ?SubscriberEntity $subscriber
|
||||
* @param array|false $oldWpUserData
|
||||
*/
|
||||
private function handleCreatingOrUpdatingSubscriber(string $currentFilter, \WP_User $wpUser, ?SubscriberEntity $subscriber = null, $oldWpUserData = false): void {
|
||||
// Add or update
|
||||
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
|
||||
|
||||
// find subscriber by email when is null
|
||||
if (is_null($subscriber)) {
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['email' => $wpUser->user_email]); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
// get first name & last name
|
||||
$firstName = html_entity_decode($wpUser->first_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$lastName = html_entity_decode($wpUser->last_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
if (empty($wpUser->first_name) && empty($wpUser->last_name)) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$firstName = html_entity_decode($wpUser->display_name); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
$signupConfirmationEnabled = SettingsController::getInstance()->get('signup_confirmation.enabled');
|
||||
$status = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
|
||||
// we want to mark a new subscriber as unsubscribe when the checkbox from registration is unchecked
|
||||
if (isset($_POST['mailpoet']['subscribe_on_register_active']) && (bool)$_POST['mailpoet']['subscribe_on_register_active'] === true) {
|
||||
$status = SubscriberEntity::STATUS_UNSUBSCRIBED;
|
||||
}
|
||||
|
||||
// subscriber data
|
||||
$data = [
|
||||
'wp_user_id' => $wpUser->ID,
|
||||
'email' => $wpUser->user_email, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'status' => $status,
|
||||
'source' => Source::WORDPRESS_USER,
|
||||
];
|
||||
|
||||
if (!is_null($subscriber)) {
|
||||
$data['id'] = $subscriber->getId();
|
||||
unset($data['status']); // don't override status for existing users
|
||||
unset($data['source']); // don't override status for existing users
|
||||
}
|
||||
|
||||
$addingNewUserToDisabledWPSegment = $wpSegment->getDeletedAt() !== null && $currentFilter === 'user_register';
|
||||
|
||||
$otherActiveSegments = [];
|
||||
if ($subscriber) {
|
||||
$otherActiveSegments = array_filter($subscriber->getSegments()->toArray() ?? [], function (SegmentEntity $segment) {
|
||||
return $segment->getType() !== SegmentEntity::TYPE_WP_USERS && $segment->getDeletedAt() === null;
|
||||
});
|
||||
}
|
||||
$isWooCustomer = $this->wooHelper->isWooCommerceActive() && in_array('customer', $wpUser->roles, true);
|
||||
// When WP Segment is disabled force trashed state and unconfirmed status for new WPUsers without active segment
|
||||
// or who are not WooCommerce customers at the same time since customers are added to the WooCommerce list
|
||||
if ($addingNewUserToDisabledWPSegment && !$otherActiveSegments && !$isWooCustomer) {
|
||||
$data['deleted_at'] = Carbon::now()->millisecond(0);
|
||||
$data['status'] = SubscriberEntity::STATUS_UNCONFIRMED;
|
||||
}
|
||||
|
||||
try {
|
||||
$subscriber = $this->createOrUpdateSubscriber($data, $subscriber);
|
||||
} catch (\Exception $e) {
|
||||
return; // fails silently as this was the behavior of this methods before the Doctrine refactor.
|
||||
}
|
||||
|
||||
// add subscriber to the WP Users segment
|
||||
$this->subscriberSegmentRepository->subscribeToSegments(
|
||||
$subscriber,
|
||||
[$wpSegment]
|
||||
);
|
||||
|
||||
if (!$signupConfirmationEnabled && $subscriber->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED && $currentFilter === 'user_register') {
|
||||
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy([
|
||||
'subscriber' => $subscriber->getId(),
|
||||
'segment' => $wpSegment->getId(),
|
||||
]);
|
||||
|
||||
if (!is_null($subscriberSegment)) {
|
||||
$this->wp->doAction('mailpoet_segment_subscribed', $subscriberSegment);
|
||||
}
|
||||
}
|
||||
|
||||
$subscribeOnRegisterEnabled = SettingsController::getInstance()->get('subscribe.on_register.enabled');
|
||||
$sendConfirmationEmail =
|
||||
$signupConfirmationEnabled
|
||||
&& $subscribeOnRegisterEnabled
|
||||
&& $currentFilter !== 'profile_update'
|
||||
&& !$addingNewUserToDisabledWPSegment;
|
||||
|
||||
if ($sendConfirmationEmail && ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED)) {
|
||||
/** @var ConfirmationEmailMailer $confirmationEmailMailer */
|
||||
$confirmationEmailMailer = ContainerWrapper::getInstance()->get(ConfirmationEmailMailer::class);
|
||||
try {
|
||||
$confirmationEmailMailer->sendConfirmationEmailOnce($subscriber);
|
||||
} catch (\Exception $e) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// welcome email
|
||||
$scheduleWelcomeNewsletter = false;
|
||||
if (in_array($currentFilter, ['profile_update', 'user_register', 'add_user_role', 'set_user_role'])) {
|
||||
$scheduleWelcomeNewsletter = true;
|
||||
}
|
||||
if ($scheduleWelcomeNewsletter === true) {
|
||||
$this->welcomeScheduler->scheduleWPUserWelcomeNotification(
|
||||
$subscriber->getId(),
|
||||
(array)$wpUser,
|
||||
(array)$oldWpUserData
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createOrUpdateSubscriber(array $data, ?SubscriberEntity $subscriber = null): SubscriberEntity {
|
||||
if (is_null($subscriber)) {
|
||||
$subscriber = new SubscriberEntity();
|
||||
}
|
||||
|
||||
$subscriber->setWpUserId($data['wp_user_id']);
|
||||
$subscriber->setEmail($data['email']);
|
||||
$subscriber->setFirstName($data['first_name']);
|
||||
$subscriber->setLastName($data['last_name']);
|
||||
|
||||
if (isset($data['status'])) {
|
||||
$subscriber->setStatus($data['status']);
|
||||
}
|
||||
|
||||
if (isset($data['source'])) {
|
||||
$subscriber->setSource($data['source']);
|
||||
}
|
||||
|
||||
if (isset($data['deleted_at'])) {
|
||||
$subscriber->setDeletedAt($data['deleted_at']);
|
||||
}
|
||||
|
||||
$this->subscribersRepository->persist($subscriber);
|
||||
$this->subscribersRepository->flush();
|
||||
|
||||
return $subscriber;
|
||||
}
|
||||
|
||||
public function synchronizeUsers(): bool {
|
||||
// Temporarily skip synchronization in WP Playground.
|
||||
// Some of the queries are not yet supported by the SQLite integration.
|
||||
if (Connection::isSQLite()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save timestamp about changes and update before insert
|
||||
$this->subscriberChangesNotifier->subscribersBatchCreate();
|
||||
$this->subscriberChangesNotifier->subscribersBatchUpdate();
|
||||
|
||||
$updatedUsersEmails = $this->updateSubscribersEmails();
|
||||
$insertedUsersEmails = $this->insertSubscribers();
|
||||
$this->removeUpdatedSubscribersWithInvalidEmail(array_merge($updatedUsersEmails, $insertedUsersEmails));
|
||||
// There is high chance that an update will be made
|
||||
$this->subscriberChangesNotifier->subscribersBatchUpdate();
|
||||
unset($updatedUsersEmails);
|
||||
unset($insertedUsersEmails);
|
||||
$this->updateFirstNames();
|
||||
$this->updateLastNames();
|
||||
$this->updateFirstNameIfMissing();
|
||||
$this->insertUsersToSegment();
|
||||
$this->removeOrphanedSubscribers();
|
||||
$this->subscribersRepository->invalidateTotalSubscribersCache();
|
||||
$this->subscribersRepository->refreshAll();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function removeUpdatedSubscribersWithInvalidEmail(array $updatedEmails): void {
|
||||
$invalidWpUserIds = array_map(function($item) {
|
||||
return $item['id'];
|
||||
},
|
||||
array_filter($updatedEmails, function($updatedEmail) {
|
||||
return !$this->validator->validateEmail($updatedEmail['email']) && $updatedEmail['id'] !== null;
|
||||
}));
|
||||
if (!$invalidWpUserIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->subscribersRepository->removeByWpUserIds($invalidWpUserIds);
|
||||
}
|
||||
|
||||
private function updateSubscribersEmails(): array {
|
||||
global $wpdb;
|
||||
|
||||
$stmt = $this->databaseConnection->executeQuery('SELECT NOW();');
|
||||
$startTime = $stmt->fetchOne();
|
||||
|
||||
if (!is_string($startTime)) {
|
||||
throw new \RuntimeException("Failed to fetch the current time.");
|
||||
}
|
||||
|
||||
$updateSql =
|
||||
"UPDATE IGNORE {$this->subscribersTable} s
|
||||
INNER JOIN {$wpdb->users} as wu ON s.wp_user_id = wu.id
|
||||
SET s.email = wu.user_email";
|
||||
$this->databaseConnection->executeStatement($updateSql);
|
||||
|
||||
$selectSql =
|
||||
"SELECT wp_user_id as id, email FROM {$this->subscribersTable}
|
||||
WHERE updated_at >= '{$startTime}'";
|
||||
$updatedEmails = $this->databaseConnection->fetchAllAssociative($selectSql);
|
||||
|
||||
return $updatedEmails;
|
||||
}
|
||||
|
||||
private function insertSubscribers(): array {
|
||||
global $wpdb;
|
||||
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
|
||||
|
||||
if ($wpSegment->getDeletedAt() !== null) {
|
||||
$subscriberStatus = SubscriberEntity::STATUS_UNCONFIRMED;
|
||||
$deletedAt = 'CURRENT_TIMESTAMP()';
|
||||
} else {
|
||||
$signupConfirmationEnabled = SettingsController::getInstance()->get('signup_confirmation.enabled');
|
||||
$subscriberStatus = $signupConfirmationEnabled ? SubscriberEntity::STATUS_UNCONFIRMED : SubscriberEntity::STATUS_SUBSCRIBED;
|
||||
$deletedAt = 'null';
|
||||
}
|
||||
|
||||
// Fetch users that are not in the subscribers table
|
||||
$selectSql =
|
||||
"SELECT u.id, u.user_email as email
|
||||
FROM {$wpdb->users} u
|
||||
LEFT JOIN {$this->subscribersTable} AS s ON s.wp_user_id = u.id
|
||||
WHERE s.wp_user_id IS NULL AND u.user_email != ''";
|
||||
$insertedUserIds = $this->databaseConnection->fetchAllAssociative($selectSql);
|
||||
|
||||
// Insert new users into the subscribers table
|
||||
$insertSql =
|
||||
"INSERT IGNORE INTO {$this->subscribersTable} (wp_user_id, email, status, created_at, `source`, deleted_at)
|
||||
SELECT wu.id, wu.user_email, :subscriberStatus, CURRENT_TIMESTAMP(), :source, {$deletedAt}
|
||||
FROM {$wpdb->users} wu
|
||||
LEFT JOIN {$this->subscribersTable} s ON wu.id = s.wp_user_id
|
||||
WHERE s.wp_user_id IS NULL AND wu.user_email != ''
|
||||
ON DUPLICATE KEY UPDATE wp_user_id = wu.id";
|
||||
$stmt = $this->databaseConnection->prepare($insertSql);
|
||||
$stmt->bindValue('subscriberStatus', $subscriberStatus);
|
||||
$stmt->bindValue('source', Source::WORDPRESS_USER);
|
||||
$stmt->executeStatement();
|
||||
|
||||
return $insertedUserIds;
|
||||
}
|
||||
|
||||
private function updateFirstNames(): void {
|
||||
global $wpdb;
|
||||
|
||||
$sql =
|
||||
"UPDATE {$this->subscribersTable} s
|
||||
JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'first_name'
|
||||
SET s.first_name = SUBSTRING(wpum.meta_value, 1, 255)
|
||||
WHERE s.first_name = ''
|
||||
AND s.wp_user_id IS NOT NULL
|
||||
AND wpum.meta_value IS NOT NULL";
|
||||
|
||||
$this->databaseConnection->executeStatement($sql);
|
||||
}
|
||||
|
||||
private function updateLastNames(): void {
|
||||
global $wpdb;
|
||||
|
||||
$sql =
|
||||
"UPDATE {$this->subscribersTable} s
|
||||
JOIN {$wpdb->usermeta} as wpum ON s.wp_user_id = wpum.user_id AND wpum.meta_key = 'last_name'
|
||||
SET s.last_name = SUBSTRING(wpum.meta_value, 1, 255)
|
||||
WHERE s.last_name = ''
|
||||
AND s.wp_user_id IS NOT NULL
|
||||
AND wpum.meta_value IS NOT NULL";
|
||||
|
||||
$this->databaseConnection->executeStatement($sql);
|
||||
}
|
||||
|
||||
private function updateFirstNameIfMissing(): void {
|
||||
global $wpdb;
|
||||
|
||||
$sql =
|
||||
"UPDATE {$this->subscribersTable} s
|
||||
JOIN {$wpdb->users} wu ON s.wp_user_id = wu.id
|
||||
SET s.first_name = wu.display_name
|
||||
WHERE s.first_name = ''
|
||||
AND s.wp_user_id IS NOT NULL";
|
||||
|
||||
$this->databaseConnection->executeStatement($sql);
|
||||
}
|
||||
|
||||
private function insertUsersToSegment(): void {
|
||||
$wpSegment = $this->segmentsRepository->getWPUsersSegment();
|
||||
$subscribersSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
|
||||
$sql =
|
||||
"INSERT IGNORE INTO {$subscribersSegmentTable} (subscriber_id, segment_id, created_at)
|
||||
SELECT s.id, '{$wpSegment->getId()}', CURRENT_TIMESTAMP() FROM {$this->subscribersTable} s
|
||||
WHERE s.wp_user_id > 0";
|
||||
|
||||
$this->databaseConnection->executeStatement($sql);
|
||||
}
|
||||
|
||||
private function removeOrphanedSubscribers(): void {
|
||||
$this->subscribersRepository->removeOrphanedSubscribersFromWpSegment();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoet\Config\SubscriberChangesNotifier;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\Services\Validator;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\Subscribers\Source;
|
||||
use MailPoet\Subscribers\SubscriberSaveController;
|
||||
use MailPoet\Subscribers\SubscriberSegmentRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\WooCommerce\Helper as WCHelper;
|
||||
use MailPoet\WooCommerce\Subscription;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Connection;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerce {
|
||||
/** @var SettingsController */
|
||||
private $settings;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WP */
|
||||
private $wpSegment;
|
||||
|
||||
/** @var string|null */
|
||||
private $mailpoetEmailCollation;
|
||||
|
||||
/** @var string|null */
|
||||
private $wpPostmetaValueCollation;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var SubscriberSegmentRepository */
|
||||
private $subscriberSegmentRepository;
|
||||
|
||||
/** @var SubscriberSaveController */
|
||||
private $subscriberSaveController;
|
||||
|
||||
/** @var WCHelper */
|
||||
private $woocommerceHelper;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var SubscriberChangesNotifier */
|
||||
private $subscriberChangesNotifier;
|
||||
|
||||
/** @var Validator */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
SettingsController $settings,
|
||||
WPFunctions $wp,
|
||||
WCHelper $woocommerceHelper,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
SubscriberSegmentRepository $subscriberSegmentRepository,
|
||||
SubscriberSaveController $subscriberSaveController,
|
||||
WP $wpSegment,
|
||||
EntityManager $entityManager,
|
||||
Connection $connection,
|
||||
SubscriberChangesNotifier $subscriberChangesNotifier,
|
||||
Validator $validator
|
||||
) {
|
||||
$this->settings = $settings;
|
||||
$this->wp = $wp;
|
||||
$this->wpSegment = $wpSegment;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->subscriberSegmentRepository = $subscriberSegmentRepository;
|
||||
$this->subscriberSaveController = $subscriberSaveController;
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->connection = $connection;
|
||||
$this->subscriberChangesNotifier = $subscriberChangesNotifier;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function shouldShowWooCommerceSegment(): bool {
|
||||
$isWoocommerceActive = $this->woocommerceHelper->isWooCommerceActive();
|
||||
$woocommerceUserExists = $this->subscribersRepository->woocommerceUserExists();
|
||||
|
||||
if (!$isWoocommerceActive && !$woocommerceUserExists) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function synchronizeRegisteredCustomer(int $wpUserId, ?string $currentFilter = null): bool {
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
|
||||
$currentFilter = $currentFilter ?: $this->wp->currentFilter();
|
||||
switch ($currentFilter) {
|
||||
case 'woocommerce_delete_customer':
|
||||
// subscriber should be already deleted in WP users sync
|
||||
$this->unsubscribeUsersFromSegment(); // remove leftover association
|
||||
break;
|
||||
case 'woocommerce_new_customer':
|
||||
case 'woocommerce_created_customer':
|
||||
$newCustomer = true;
|
||||
case 'woocommerce_update_customer':
|
||||
default:
|
||||
$wpUser = $this->wp->getUserdata($wpUserId);
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['wpUserId' => $wpUserId]);
|
||||
|
||||
if ($wpUser === false || $subscriber === null) {
|
||||
// registered customers should exist as WP users and WP segment subscribers
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'is_woocommerce_user' => 1,
|
||||
];
|
||||
if (!empty($newCustomer)) {
|
||||
$data['source'] = Source::WOOCOMMERCE_USER;
|
||||
}
|
||||
$data['id'] = $subscriber->getId();
|
||||
if ($wpUser->first_name) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$data['first_name'] = $wpUser->first_name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
if ($wpUser->last_name) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$data['last_name'] = $wpUser->last_name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
$subscriber = $this->subscriberSaveController->createOrUpdate($data, $subscriber);
|
||||
// add subscriber to the WooCommerce Customers segment when relation doesn't exist
|
||||
$subscriberSegment = $this->subscriberSegmentRepository->findOneBy(['subscriber' => $subscriber, 'segment' => $wcSegment]);
|
||||
|
||||
if (!$subscriberSegment && $this->shouldSubscribeToWooSegment()) {
|
||||
$this->subscriberSegmentRepository->subscribeToSegments(
|
||||
$subscriber,
|
||||
[$wcSegment]
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should subscribe to the Woo segment when creating a new woo customer and not on checkout
|
||||
* or when on checkout and MailPoet subscribe optin is enabled and checked.
|
||||
*/
|
||||
protected function shouldSubscribeToWooSegment(): bool {
|
||||
$checkoutOptinEnabled = (bool)$this->settings->get(Subscription::OPTIN_ENABLED_SETTING_NAME);
|
||||
$checkoutOptinChecked = !empty($_POST[Subscription::CHECKOUT_OPTIN_INPUT_NAME]);
|
||||
|
||||
return !$this->woocommerceHelper->isCheckoutRequest() || ($checkoutOptinEnabled && $checkoutOptinChecked);
|
||||
}
|
||||
|
||||
public function synchronizeGuestCustomer(int $orderId): void {
|
||||
$wcOrder = $this->woocommerceHelper->wcGetOrder($orderId);
|
||||
|
||||
if (!$wcOrder instanceof \WC_Order) return;
|
||||
$signupConfirmation = $this->settings->get('signup_confirmation');
|
||||
$status = SubscriberEntity::STATUS_UNSUBSCRIBED;
|
||||
if ((bool)$signupConfirmation['enabled'] === false) {
|
||||
$status = SubscriberEntity::STATUS_SUBSCRIBED;
|
||||
}
|
||||
|
||||
$email = $this->insertSubscriberFromOrder($wcOrder, $status);
|
||||
|
||||
if (empty($email)) {
|
||||
return;
|
||||
}
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['email' => $email]);
|
||||
|
||||
if ($subscriber) {
|
||||
$firstName = $wcOrder->get_billing_first_name(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$lastName = $wcOrder->get_billing_last_name(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
if ($firstName) {
|
||||
$subscriber->setFirstName($firstName);
|
||||
}
|
||||
if ($lastName) {
|
||||
$subscriber->setLastName($lastName);
|
||||
}
|
||||
if ($firstName || $lastName) {
|
||||
$this->subscribersRepository->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function synchronizeCustomers(int $lastCheckedOrderId = 0, ?int $highestOrderId = null, int $batchSize = 1000): int {
|
||||
|
||||
$this->wpSegment->synchronizeUsers(); // synchronize registered users
|
||||
|
||||
$this->markRegisteredCustomers();
|
||||
|
||||
$processedOrders = $this->insertSubscribersFromOrders($lastCheckedOrderId, $batchSize);
|
||||
$this->updateNames($processedOrders);
|
||||
|
||||
$lastCheckedOrderId = $lastCheckedOrderId + $batchSize;
|
||||
if (!$highestOrderId || $lastCheckedOrderId >= $highestOrderId) {
|
||||
$this->insertUsersToSegment();
|
||||
$this->unsubscribeUsersFromSegment();
|
||||
$this->removeOrphanedSubscribers();
|
||||
$this->updateStatus();
|
||||
$this->updateGlobalStatus();
|
||||
}
|
||||
|
||||
$this->subscribersRepository->invalidateTotalSubscribersCache();
|
||||
return $lastCheckedOrderId;
|
||||
}
|
||||
|
||||
private function ensureColumnCollation(): void {
|
||||
if ($this->mailpoetEmailCollation && $this->wpPostmetaValueCollation) {
|
||||
return;
|
||||
}
|
||||
global $wpdb;
|
||||
|
||||
$mailpoetEmailColumn = $wpdb->get_row($wpdb->prepare(
|
||||
"SHOW FULL COLUMNS FROM %i WHERE Field = 'email'",
|
||||
$this->subscribersRepository->getTableName()
|
||||
));
|
||||
$this->mailpoetEmailCollation = $mailpoetEmailColumn->Collation; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
$wpPostmetaValueColumn = $wpdb->get_row($wpdb->prepare(
|
||||
"SHOW FULL COLUMNS FROM %i WHERE Field = 'meta_value'",
|
||||
$wpdb->postmeta
|
||||
));
|
||||
$this->wpPostmetaValueCollation = $wpPostmetaValueColumn->Collation; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
/**
|
||||
* In MySQL, if you have the same charset and collation in joined tables' columns it's perfect;
|
||||
* if you have different charsets, utf8 and utf8mb4, it works too; but if you have the same charset
|
||||
* with different collations, e.g. utf8mb4_unicode_ci and utf8mb4_unicode_520_ci, it will fail
|
||||
* with an 'Illegal mix of collations' error. That's why we need an optional COLLATE clause to fix this.
|
||||
*/
|
||||
private function needsCollationChange(): bool {
|
||||
$this->ensureColumnCollation();
|
||||
$collation1 = (string)$this->mailpoetEmailCollation;
|
||||
$collation2 = (string)$this->wpPostmetaValueCollation;
|
||||
|
||||
if ($collation1 === $collation2) {
|
||||
return false;
|
||||
}
|
||||
[$charset1] = explode('_', $collation1);
|
||||
[$charset2] = explode('_', $collation2);
|
||||
|
||||
return $charset1 === $charset2;
|
||||
}
|
||||
|
||||
private function markRegisteredCustomers(): void {
|
||||
// Mark WP users having a customer role as WooCommerce subscribers
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$this->connection->executeQuery("
|
||||
UPDATE LOW_PRIORITY {$subscribersTable} mps
|
||||
JOIN {$wpdb->users} wu ON mps.wp_user_id = wu.id
|
||||
JOIN {$wpdb->usermeta} wpum ON wu.id = wpum.user_id AND wpum.meta_key = :capabilities
|
||||
SET is_woocommerce_user = 1, source = :source
|
||||
WHERE wpum.meta_value LIKE '%\"customer\"%'
|
||||
", ['capabilities' => $wpdb->prefix . 'capabilities', 'source' => Source::WOOCOMMERCE_USER]);
|
||||
}
|
||||
|
||||
private function insertSubscriberFromOrder(\WC_Order $wcOrder, string $status): ?string {
|
||||
$email = $wcOrder->get_billing_email();
|
||||
|
||||
if (!$email || !$this->validator->validateEmail($email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->insertSubscribers([$email], $status);
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function insertSubscribersFromOrders(int $lastProcessedOrderId, int $batchSize): array {
|
||||
global $wpdb;
|
||||
|
||||
$parameters = [
|
||||
'lowestOrderId' => $lastProcessedOrderId,
|
||||
'highestOrderId' => $lastProcessedOrderId + $batchSize,
|
||||
];
|
||||
$parametersType = [
|
||||
'lowestOrderId' => ParameterType::INTEGER,
|
||||
'highestOrderId' => ParameterType::INTEGER,
|
||||
];
|
||||
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$ordersTable = $this->woocommerceHelper->getOrdersTableName();
|
||||
$query = "SELECT id AS order_id, billing_email AS email
|
||||
FROM `{$ordersTable}`
|
||||
WHERE type = 'shop_order' AND billing_email != '' AND (id > :lowestOrderId AND id <= :highestOrderId)
|
||||
ORDER BY id";
|
||||
} else {
|
||||
$query = "SELECT wpp.id AS order_id, wppm.meta_value AS email
|
||||
FROM `{$wpdb->posts}` wpp
|
||||
JOIN `{$wpdb->postmeta}` wppm ON wpp.ID = wppm.post_id AND wppm.meta_key = '_billing_email' AND wppm.meta_value != ''
|
||||
WHERE wpp.post_type = 'shop_order'
|
||||
AND (wpp.ID > :lowestOrderId AND wpp.ID <= :highestOrderId)
|
||||
ORDER BY wpp.id";
|
||||
}
|
||||
|
||||
$result = $this->connection->executeQuery($query, $parameters, $parametersType)->fetchAllAssociative();
|
||||
|
||||
$processedOrders = [];
|
||||
foreach ($result as $item) {
|
||||
if (!$this->validator->validateEmail($item['email'])) {
|
||||
continue;
|
||||
}
|
||||
// because data in result are sorted by id, we can replace the previous order id
|
||||
$processedOrders[(string)$item['email']] = (int)$item['order_id'];
|
||||
}
|
||||
|
||||
if (count($processedOrders)) {
|
||||
$this->insertSubscribers(array_keys($processedOrders));
|
||||
}
|
||||
|
||||
return $processedOrders;
|
||||
}
|
||||
|
||||
private function insertSubscribers(array $emails, string $status = SubscriberEntity::STATUS_SUBSCRIBED): int {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscribersValues = [];
|
||||
$now = Carbon::now()->format('Y-m-d H:i:s');
|
||||
$source = Source::WOOCOMMERCE_USER;
|
||||
foreach ($emails as $email) {
|
||||
/** @var string $email */
|
||||
$email = $this->connection->quote($email);
|
||||
$email = strval($email);
|
||||
$subscribersValues[] = "(1, {$email}, '{$status}', '{$now}', '{$now}', '{$source}')";
|
||||
}
|
||||
|
||||
// Save timestamp about changes before insert
|
||||
$this->subscriberChangesNotifier->subscribersBatchUpdate();
|
||||
// Update existing subscribers
|
||||
$this->connection->executeQuery('
|
||||
UPDATE ' . $subscribersTable . ' mps
|
||||
SET mps.is_woocommerce_user = 1
|
||||
WHERE mps.email IN (:emails)
|
||||
', ['emails' => $emails], ['emails' => ArrayParameterType::STRING]);
|
||||
|
||||
// Save timestamp about new subscribers before insert
|
||||
$this->subscriberChangesNotifier->subscribersBatchCreate();
|
||||
// Insert new subscribers
|
||||
$this->connection->executeQuery('
|
||||
INSERT IGNORE INTO ' . $subscribersTable . ' (`is_woocommerce_user`, `email`, `status`, `created_at`, `last_subscribed_at`, `source`) VALUES
|
||||
' . implode(',', $subscribersValues) . '
|
||||
');
|
||||
|
||||
return count($emails);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $orders
|
||||
*/
|
||||
private function updateNames(array $orders): void {
|
||||
global $wpdb;
|
||||
if (!$orders) {
|
||||
return;
|
||||
}
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$addressesTableName = $this->woocommerceHelper->getAddressesTableName();
|
||||
$metaData = [];
|
||||
$results = $this->connection->executeQuery(
|
||||
"
|
||||
SELECT order_id, first_name, last_name
|
||||
FROM {$addressesTableName}
|
||||
WHERE order_id IN (:orderIds) and address_type = 'billing'",
|
||||
['orderIds' => array_values($orders)],
|
||||
['orderIds' => ArrayParameterType::INTEGER]
|
||||
)->fetchAllAssociative();
|
||||
|
||||
// format data in the same format that is used when querying wp_postmeta (see below).
|
||||
foreach ($results as $result) {
|
||||
$firstNameData['post_id'] = $result['order_id'];
|
||||
$firstNameData['meta_key'] = '_billing_first_name';
|
||||
$firstNameData['meta_value'] = $result['first_name'];
|
||||
$metaData[] = $firstNameData;
|
||||
|
||||
$lastNameData['post_id'] = $result['order_id'];
|
||||
$lastNameData['meta_key'] = '_billing_last_name';
|
||||
$lastNameData['meta_value'] = $result['last_name'];
|
||||
$metaData[] = $lastNameData;
|
||||
}
|
||||
} else {
|
||||
$metaKeys = [
|
||||
'_billing_first_name',
|
||||
'_billing_last_name',
|
||||
];
|
||||
$metaData = $this->connection->executeQuery(
|
||||
"
|
||||
SELECT post_id, meta_key, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key IN ('_billing_first_name', '_billing_last_name') AND post_id IN (:postIds)
|
||||
",
|
||||
['metaKeys' => $metaKeys, 'postIds' => array_values($orders)],
|
||||
['metaKeys' => ArrayParameterType::STRING, 'postIds' => ArrayParameterType::INTEGER]
|
||||
)->fetchAllAssociative();
|
||||
}
|
||||
|
||||
$subscribersData = [];
|
||||
foreach ($orders as $email => $postId) {
|
||||
$subscribersData[$postId]['email'] = $email;
|
||||
}
|
||||
|
||||
foreach ($metaData as $row) {
|
||||
if (!$row['meta_value']) {
|
||||
continue;
|
||||
}
|
||||
$subscribersData[$row['post_id']][$row['meta_key']] = $row['meta_value'];
|
||||
}
|
||||
|
||||
$now = (Carbon::now())->format('Y-m-d H:i:s');
|
||||
foreach ($subscribersData as $subscriber) {
|
||||
$data = [];
|
||||
$data['woocommerce_synced_at'] = $now;
|
||||
if (!empty($subscriber['_billing_first_name'])) $data['first_name'] = $subscriber['_billing_first_name'];
|
||||
if (!empty($subscriber['_billing_last_name'])) $data['last_name'] = $subscriber['_billing_last_name'];
|
||||
$this->connection->update($subscribersTable, $data, ['email' => $subscriber['email']]);
|
||||
}
|
||||
}
|
||||
|
||||
private function insertUsersToSegment(): void {
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
// Subscribe WC users to segment
|
||||
$this->connection->executeQuery(
|
||||
"
|
||||
INSERT IGNORE INTO {$subscriberSegmentsTable} (subscriber_id, segment_id, created_at)
|
||||
SELECT id, :segmentId, CURRENT_TIMESTAMP()
|
||||
FROM {$subscribersTable}
|
||||
WHERE is_woocommerce_user = 1
|
||||
",
|
||||
['segmentId' => $wcSegment->getId()],
|
||||
['segmentId' => ParameterType::INTEGER]
|
||||
);
|
||||
}
|
||||
|
||||
private function unsubscribeUsersFromSegment(): void {
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
// Unsubscribe non-WC or invalid users from segment
|
||||
$this->connection->executeQuery(
|
||||
"
|
||||
DELETE mpss FROM {$subscriberSegmentsTable} mpss
|
||||
LEFT JOIN {$subscribersTable} mps ON mpss.subscriber_id = mps.id
|
||||
WHERE mpss.segment_id = :segmentId AND (mps.is_woocommerce_user = 0 OR mps.email = '' OR mps.email IS NULL)
|
||||
",
|
||||
['segmentId' => $wcSegment->getId()],
|
||||
['segmentId' => ParameterType::INTEGER]
|
||||
);
|
||||
}
|
||||
|
||||
private function updateGlobalStatus(): void {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
// Set global status unsubscribed to all woocommerce users without any segment
|
||||
$this->connection->executeQuery(
|
||||
"
|
||||
UPDATE {$subscribersTable} mps
|
||||
LEFT JOIN {$subscriberSegmentsTable} mpss ON mpss.subscriber_id = mps.id
|
||||
SET mps.status = :statusUnsubscribed
|
||||
WHERE mpss.id IS NULL
|
||||
AND mps.is_woocommerce_user = 1
|
||||
",
|
||||
['statusUnsubscribed' => SubscriberEntity::STATUS_UNSUBSCRIBED],
|
||||
['statusUnsubscribed' => ParameterType::INTEGER]
|
||||
);
|
||||
// SET global status unsubscribed to all woocommerce users who have only 1 segment and it is woocommerce segment and they are not subscribed
|
||||
// You can't specify target table 'mps' for update in FROM clause
|
||||
$this->connection->executeQuery(
|
||||
"
|
||||
UPDATE {$subscribersTable} mps
|
||||
JOIN {$subscriberSegmentsTable} mpss ON mps.id = mpss.subscriber_id AND mpss.segment_id = :segmentId AND mpss.status = :statusUnsubscribed
|
||||
SET mps.status = :statusUnsubscribed
|
||||
WHERE mps.id IN (
|
||||
SELECT s.id -- get all subscribers with exactly 1 segment
|
||||
FROM (SELECT id FROM {$subscribersTable} WHERE is_woocommerce_user = 1) s
|
||||
JOIN {$subscriberSegmentsTable} ss on s.id = ss.subscriber_id
|
||||
GROUP BY s.id
|
||||
HAVING COUNT(ss.id) = 1
|
||||
)
|
||||
",
|
||||
['statusUnsubscribed' => SubscriberEntity::STATUS_UNSUBSCRIBED, 'segmentId' => $wcSegment->getId()],
|
||||
['statusUnsubscribed' => ParameterType::STRING, 'segmentId' => ParameterType::INTEGER]
|
||||
);
|
||||
}
|
||||
|
||||
private function removeOrphanedSubscribers(): void {
|
||||
// Remove orphaned WooCommerce segment subscribers (not having a matching WC customer email),
|
||||
// e.g. if WC orders were deleted directly from the database
|
||||
// or a customer role was revoked and a user has no orders
|
||||
global $wpdb;
|
||||
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
|
||||
// Unmark registered customers
|
||||
|
||||
// Insert WC customer IDs to a temporary table for left join to use an index
|
||||
$tmpTableName = Env::$dbPrefix . 'tmp_wc_ids';
|
||||
// Registered users with orders
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$ordersTable = $this->woocommerceHelper->getOrdersTableName();
|
||||
$registeredCustomersSubQuery = "SELECT DISTINCT customer_id AS id FROM `{$ordersTable}` WHERE type = 'shop_order'";
|
||||
} else {
|
||||
$registeredCustomersSubQuery = "SELECT DISTINCT wppm.meta_value AS id FROM {$wpdb->postmeta} wppm
|
||||
JOIN {$wpdb->posts} wpp ON wppm.post_id = wpp.ID
|
||||
AND wpp.post_type = 'shop_order'
|
||||
WHERE wppm.meta_key = '_customer_user'";
|
||||
}
|
||||
|
||||
$this->connection->executeQuery("
|
||||
CREATE TEMPORARY TABLE {$tmpTableName}
|
||||
(`id` int(11) unsigned NOT NULL, UNIQUE(`id`), PRIMARY KEY (`id`)) AS
|
||||
{$registeredCustomersSubQuery}
|
||||
");
|
||||
// Registered users with a customer role
|
||||
$this->connection->executeQuery("
|
||||
INSERT IGNORE INTO {$tmpTableName}
|
||||
SELECT DISTINCT wpum.user_id AS id FROM {$wpdb->usermeta} wpum
|
||||
WHERE wpum.meta_key = :capabilities AND wpum.meta_value LIKE '%\"customer\"%'
|
||||
", ['capabilities' => $wpdb->prefix . 'capabilities']);
|
||||
|
||||
// Unmark WC list registered users which aren't WC customers anymore
|
||||
$subQb = $this->connection->createQueryBuilder();
|
||||
$subQb->select('mps.id')
|
||||
->from($subscribersTable, 'mps')
|
||||
->join('mps', $subscriberSegmentsTable, 'mpss', 'mps.id = mpss.subscriber_id AND mpss.segment_id = :segmentId')
|
||||
->leftJoin('mps', $tmpTableName, 'wctmp', 'mps.wp_user_id = wctmp.id')
|
||||
->where('mps.is_woocommerce_user = 1')
|
||||
->andWhere('wctmp.id IS NULL')
|
||||
->andWhere('mps.wp_user_id IS NOT NULL');
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update($subscribersTable)
|
||||
->set('is_woocommerce_user', '0')
|
||||
->where("id IN (SELECT id FROM ({$subQb->getSQL()}) AS sq) ")
|
||||
->setParameter('segmentId', $wcSegment->getId());
|
||||
$qb->execute();
|
||||
|
||||
$this->connection->executeQuery("DROP TABLE {$tmpTableName}");
|
||||
|
||||
// Remove guest customers
|
||||
|
||||
// Insert WC customer emails to a temporary table and ensure matching collations
|
||||
// between MailPoet and WooCommerce emails for left join to use an index
|
||||
$tmpTableName = Env::$dbPrefix . 'tmp_wc_emails';
|
||||
if ($this->needsCollationChange()) {
|
||||
$collation = "COLLATE $this->mailpoetEmailCollation";
|
||||
} else {
|
||||
$collation = "COLLATE $this->wpPostmetaValueCollation";
|
||||
}
|
||||
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$ordersTable = $this->woocommerceHelper->getOrdersTableName();
|
||||
$guestCustomersSubQuery = "SELECT DISTINCT billing_email AS email FROM `{$ordersTable}` WHERE type = 'shop_order' AND billing_email IS NOT NULL AND billing_email != ''";
|
||||
} else {
|
||||
$guestCustomersSubQuery = "SELECT DISTINCT wppm.meta_value AS email FROM {$wpdb->postmeta} wppm
|
||||
JOIN {$wpdb->posts} wpp ON wppm.post_id = wpp.ID
|
||||
AND wpp.post_type = 'shop_order'
|
||||
WHERE wppm.meta_key = '_billing_email'";
|
||||
}
|
||||
|
||||
$this->connection->executeQuery("
|
||||
CREATE TEMPORARY TABLE {$tmpTableName}
|
||||
(`email` varchar(150) NOT NULL, UNIQUE(`email`), PRIMARY KEY (`email`)) {$collation}
|
||||
{$guestCustomersSubQuery}
|
||||
");
|
||||
|
||||
// Remove WC list guest users which aren't WC customers anymore
|
||||
$subQb = $this->connection->createQueryBuilder();
|
||||
$subQb->select('mps.id')
|
||||
->from($subscribersTable, 'mps')
|
||||
->join('mps', $subscriberSegmentsTable, 'mpss', 'mps.id = mpss.subscriber_id AND mpss.segment_id = :segmentId')
|
||||
->leftJoin('mps', $tmpTableName, 'wctmp', 'mps.email = wctmp.email')
|
||||
->where('mps.is_woocommerce_user = 1')
|
||||
->andWhere('wctmp.email IS NULL')
|
||||
->andWhere('mps.wp_user_id IS NULL');
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->delete($subscribersTable)
|
||||
->where("id IN (SELECT id FROM ({$subQb->getSQL()}) AS sq) ")
|
||||
->setParameter('segmentId', $wcSegment->getId());
|
||||
$qb->execute();
|
||||
|
||||
$this->connection->executeQuery("DROP TABLE {$tmpTableName}");
|
||||
}
|
||||
|
||||
private function updateStatus(): void {
|
||||
$subscribeOldCustomers = $this->settings->get('mailpoet_subscribe_old_woocommerce_customers.enabled', false);
|
||||
if ($subscribeOldCustomers !== "1") {
|
||||
$status = SubscriberEntity::STATUS_UNSUBSCRIBED;
|
||||
} else {
|
||||
$status = SubscriberEntity::STATUS_SUBSCRIBED;
|
||||
}
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentsTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
$wcSegment = $this->segmentsRepository->getWooCommerceSegment();
|
||||
|
||||
$this->connection->executeQuery(
|
||||
"
|
||||
UPDATE LOW_PRIORITY {$subscriberSegmentsTable} AS mpss
|
||||
JOIN {$subscribersTable} AS mps ON mpss.subscriber_id = mps.id
|
||||
SET mpss.status = :status
|
||||
WHERE
|
||||
mpss.segment_id = :segmentId
|
||||
AND mps.confirmed_at IS NULL
|
||||
AND mps.confirmed_ip IS NULL
|
||||
AND mps.is_woocommerce_user = 1
|
||||
",
|
||||
['status' => $status, 'segmentId' => $wcSegment->getId()],
|
||||
['status' => ParameterType::STRING, 'segmentId' => ParameterType::INTEGER]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user