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