init
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Repository;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @extends Repository<DynamicSegmentFilterEntity>
|
||||
*/
|
||||
class DynamicSegmentFilterRepository extends Repository {
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
parent::__construct($entityManager);
|
||||
}
|
||||
|
||||
protected function getEntityClassName() {
|
||||
return DynamicSegmentFilterEntity::class;
|
||||
}
|
||||
|
||||
public function findOnyByFilterTypeAndAction(string $filterType, string $action): ?DynamicSegmentFilterEntity {
|
||||
return $this->entityManager->createQueryBuilder()
|
||||
->select('dsf')
|
||||
->from(DynamicSegmentFilterEntity::class, 'dsf')
|
||||
->where('dsf.filterData.filterType = :filterType')
|
||||
->andWhere('dsf.filterData.action = :action')
|
||||
->setParameter('filterType', $filterType)
|
||||
->setParameter('action', $action)
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Segments\SegmentListingRepository;
|
||||
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class DynamicSegmentsListingRepository extends SegmentListingRepository {
|
||||
protected function applyParameters(QueryBuilder $queryBuilder, array $parameters): void {
|
||||
$queryBuilder
|
||||
->andWhere('s.type = :type')
|
||||
->setParameter('type', SegmentEntity::TYPE_DYNAMIC);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\InvalidStateException;
|
||||
|
||||
class InvalidFilterException extends InvalidStateException {
|
||||
const MISSING_TYPE = 1;
|
||||
const INVALID_TYPE = 2;
|
||||
const MISSING_ROLE = 3;
|
||||
const MISSING_ACTION = 4;
|
||||
const MISSING_NEWSLETTER_ID = 5;
|
||||
const MISSING_CATEGORY_ID = 6;
|
||||
const MISSING_PRODUCT_ID = 7;
|
||||
const INVALID_EMAIL_ACTION = 8;
|
||||
const MISSING_VALUE = 9;
|
||||
const MISSING_NUMBER_OF_ORDERS_FIELDS = 10;
|
||||
const MISSING_TOTAL_SPENT_FIELDS = 11;
|
||||
const INVALID_DATE_VALUE = 12;
|
||||
const MISSING_COUNTRY = 13;
|
||||
const MISSING_FILTER = 14;
|
||||
const MISSING_OPERATOR = 15;
|
||||
const MISSING_PLAN_ID = 16;
|
||||
const MISSING_SINGLE_ORDER_VALUE_FIELDS = 17;
|
||||
const MISSING_AVERAGE_SPENT_FIELDS = 18;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,591 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\AutomationsEvents;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\DateFilterHelper;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailsReceived;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\FilterHelper;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\NumberOfClicks;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedViaForm;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceFirstOrder;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedCouponCode;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedShippingMethod;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class FilterDataMapper {
|
||||
private WPFunctions $wp;
|
||||
|
||||
private DateFilterHelper $dateFilterHelper;
|
||||
|
||||
private WooCommerceNumberOfReviews $wooCommerceNumberOfReviews;
|
||||
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
private WooCommerceUsedCouponCode $wooCommerceUsedCouponCode;
|
||||
|
||||
private WooCommerceTag $wooCommerceTag;
|
||||
|
||||
private WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp,
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooCommerceNumberOfReviews $wooCommerceNumberOfReviews,
|
||||
WooCommerceUsedCouponCode $wooCommerceUsedCouponCode,
|
||||
WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute,
|
||||
WooCommerceTag $wooCommerceTag
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews;
|
||||
$this->wooCommerceUsedCouponCode = $wooCommerceUsedCouponCode;
|
||||
$this->wooCommercePurchasedWithAttribute = $wooCommercePurchasedWithAttribute;
|
||||
$this->wooCommerceTag = $wooCommerceTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return DynamicSegmentFilterData[]
|
||||
*/
|
||||
public function map(array $data = []): array {
|
||||
if (!isset($data['filters']) || count($data['filters'] ?? []) < 1) {
|
||||
throw new InvalidFilterException('Filters are missing', InvalidFilterException::MISSING_FILTER);
|
||||
}
|
||||
$processFilter = function ($filter, $data) {
|
||||
$filter['connect'] = $data['filters_connect'] ?? DynamicSegmentFilterData::CONNECT_TYPE_AND;
|
||||
return $this->createFilter($filter);
|
||||
};
|
||||
$wpFilterName = 'mailpoet_dynamic_segments_filters_map';
|
||||
if ($this->wp->hasFilter($wpFilterName)) {
|
||||
return $this->wp->applyFilters($wpFilterName, $data, $processFilter);
|
||||
}
|
||||
$filter = reset($data['filters']);
|
||||
return [$processFilter($filter, $data)];
|
||||
}
|
||||
|
||||
private function createFilter(array $filterData): DynamicSegmentFilterData {
|
||||
if (isset($filterData['days']) && !isset($filterData['timeframe'])) {
|
||||
// Backwards compatibility for filters created before time period component had "over all time" option
|
||||
$filterData['timeframe'] = DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST;
|
||||
}
|
||||
switch ($this->getSegmentType($filterData)) {
|
||||
case DynamicSegmentFilterData::TYPE_AUTOMATIONS:
|
||||
return $this->createAutomations($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_USER_ROLE:
|
||||
return $this->createSubscriber($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_EMAIL:
|
||||
return $this->createEmail($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE:
|
||||
return $this->createWooCommerce($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP:
|
||||
return $this->createWooCommerceMembership($filterData);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION:
|
||||
return $this->createWooCommerceSubscription($filterData);
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function getSegmentType(array $data): string {
|
||||
if (!isset($data['segmentType'])) {
|
||||
throw new InvalidFilterException('Segment type is not set', InvalidFilterException::MISSING_TYPE);
|
||||
}
|
||||
return $data['segmentType'];
|
||||
}
|
||||
|
||||
private function createAutomations(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing automations filter action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (in_array($data['action'], AutomationsEvents::SUPPORTED_ACTIONS)) {
|
||||
if (
|
||||
!isset($data['operator']) || !in_array($data['operator'], [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (
|
||||
!isset($data['automation_ids'])
|
||||
|| !is_array($data['automation_ids'])
|
||||
|| count($data['automation_ids']) < 1
|
||||
) {
|
||||
throw new InvalidFilterException('Missing automation IDs', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_AUTOMATIONS, $data['action'], [
|
||||
'action' => $data['action'],
|
||||
'automation_ids' => $data['automation_ids'],
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Unknown automations action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
private function createSubscriber(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
$data['action'] = DynamicSegmentFilterData::TYPE_USER_ROLE;
|
||||
}
|
||||
if ($data['action'] === SubscriberScore::TYPE) {
|
||||
if (!isset($data['value'])) {
|
||||
throw new InvalidFilterException('Missing engagement score value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'] ?? SubscriberScore::HIGHER_THAN,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === SubscriberSegment::TYPE) {
|
||||
if (empty($data['segments'])) {
|
||||
throw new InvalidFilterException('Missing segments', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'segments' => array_map(function ($segmentId) {
|
||||
return intval($segmentId);
|
||||
}, $data['segments']),
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === MailPoetCustomFields::TYPE) {
|
||||
if (empty($data['custom_field_id'])) {
|
||||
throw new InvalidFilterException('Missing custom field id', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['custom_field_type'])) {
|
||||
throw new InvalidFilterException('Missing custom field type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData = [
|
||||
'value' => $data['value'],
|
||||
'custom_field_id' => $data['custom_field_id'],
|
||||
'custom_field_type' => $data['custom_field_type'],
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
if (!empty($data['date_type'])) {
|
||||
$filterData['date_type'] = $data['date_type'];
|
||||
}
|
||||
if (!empty($data['operator'])) {
|
||||
$filterData['operator'] = $data['operator'];
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], $filterData);
|
||||
}
|
||||
if ($data['action'] === SubscriberTag::TYPE) {
|
||||
if (empty($data['tags'])) {
|
||||
throw new InvalidFilterException('Missing tags', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'tags' => array_map(function ($tagId) {
|
||||
return intval($tagId);
|
||||
}, $data['tags']),
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if ($data['action'] === SubscriberSubscribedViaForm::TYPE) {
|
||||
if (!isset($data['form_ids']) || empty($data['form_ids'])) {
|
||||
throw new InvalidFilterException('Missing at least one form ID', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], [DynamicSegmentFilterData::OPERATOR_ANY, DynamicSegmentFilterData::OPERATOR_NONE])) {
|
||||
throw new InvalidFilterException('Missing valid operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'form_ids' => array_map(function($formId) {
|
||||
return intval($formId);
|
||||
}, $data['form_ids']),
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (in_array($data['action'], SubscriberTextField::TYPES)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'],
|
||||
'action' => $data['action'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (in_array($data['action'], SubscriberDateField::TYPES)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing date value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], $this->dateFilterHelper->getValidOperators())) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'value' => $data['value'],
|
||||
'operator' => $data['operator'],
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
if (empty($data['wordpressRole'])) {
|
||||
throw new InvalidFilterException('Missing role', InvalidFilterException::MISSING_ROLE);
|
||||
}
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_USER_ROLE, $data['action'], [
|
||||
'wordpressRole' => $data['wordpressRole'],
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createEmail(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
if (!in_array($data['action'], EmailAction::ALLOWED_ACTIONS)) {
|
||||
throw new InvalidFilterException('Invalid email action', InvalidFilterException::INVALID_EMAIL_ACTION);
|
||||
}
|
||||
if (
|
||||
($data['action'] === EmailOpensAbsoluteCountAction::TYPE)
|
||||
|| ($data['action'] === EmailOpensAbsoluteCountAction::MACHINE_TYPE)
|
||||
) {
|
||||
return $this->createEmailOpensAbsoluteCount($data);
|
||||
}
|
||||
if ($data['action'] === EmailActionClickAny::TYPE) {
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], [
|
||||
'connect' => $data['connect'],
|
||||
]);
|
||||
}
|
||||
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
'operator' => $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
];
|
||||
|
||||
if ($data['action'] === EmailsReceived::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (!isset($data['emails'])) {
|
||||
throw new InvalidFilterException('Missing email count value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData['emails'] = $data['emails'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['connect'] = $data['connect'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
|
||||
}
|
||||
|
||||
if ($data['action'] === NumberOfClicks::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (!isset($data['clicks'])) {
|
||||
throw new InvalidFilterException('Missing click count value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$filterData['clicks'] = $data['clicks'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['connect'] = $data['connect'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
return new DynamicSegmentFilterData(DynamicSegmentFilterData::TYPE_EMAIL, $data['action'], $filterData);
|
||||
}
|
||||
|
||||
if (($data['action'] === EmailAction::ACTION_CLICKED)) {
|
||||
if (empty($data['newsletter_id'])) {
|
||||
throw new InvalidFilterException('Missing newsletter id', InvalidFilterException::MISSING_NEWSLETTER_ID);
|
||||
}
|
||||
$filterData['newsletter_id'] = $data['newsletter_id'];
|
||||
} else {
|
||||
if (empty($data['newsletters']) || !is_array($data['newsletters'])) {
|
||||
throw new InvalidFilterException('Missing newsletter', InvalidFilterException::MISSING_NEWSLETTER_ID);
|
||||
}
|
||||
$filterData['newsletters'] = array_map(function ($segmentId) {
|
||||
return intval($segmentId);
|
||||
}, $data['newsletters']);
|
||||
}
|
||||
|
||||
$filterType = DynamicSegmentFilterData::TYPE_EMAIL;
|
||||
$action = $data['action'];
|
||||
if (isset($data['link_ids']) && is_array($data['link_ids'])) {
|
||||
$filterData['link_ids'] = array_map('intval', $data['link_ids']);
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createEmailOpensAbsoluteCount(array $data): DynamicSegmentFilterData {
|
||||
if (!isset($data['opens'])) {
|
||||
throw new InvalidFilterException('Missing number of opens', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData = [
|
||||
'opens' => $data['opens'],
|
||||
'days' => $data['days'] ?? 0,
|
||||
'operator' => $data['operator'] ?? 'more',
|
||||
'timeframe' => $data['timeframe'] ?? DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST, // backwards compatibility
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_EMAIL;
|
||||
$action = $data['action'];
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerce(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceCategory::ACTION_CATEGORY) {
|
||||
if (!isset($data['category_ids'])) {
|
||||
throw new InvalidFilterException('Missing category', InvalidFilterException::MISSING_CATEGORY_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['category_ids'] = $data['category_ids'];
|
||||
} elseif ($data['action'] === WooCommerceProduct::ACTION_PRODUCT) {
|
||||
if (!isset($data['product_ids'])) {
|
||||
throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['product_ids'] = $data['product_ids'];
|
||||
} elseif ($data['action'] === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) {
|
||||
if (!isset($data['country_code'])) {
|
||||
throw new InvalidFilterException('Missing country', InvalidFilterException::MISSING_COUNTRY);
|
||||
}
|
||||
$filterData['country_code'] = $data['country_code'];
|
||||
$filterData['operator'] = $data['operator'] ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
} elseif (in_array($data['action'], WooCommerceNumberOfOrders::ACTIONS)) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['number_of_orders_type'])
|
||||
|| !isset($data['number_of_orders_count']) || $data['number_of_orders_count'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_NUMBER_OF_ORDERS_FIELDS);
|
||||
}
|
||||
$filterData['number_of_orders_type'] = $data['number_of_orders_type'];
|
||||
$filterData['number_of_orders_count'] = $data['number_of_orders_count'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceNumberOfReviews::ACTION) {
|
||||
$this->wooCommerceNumberOfReviews->validateFilterData($data);
|
||||
$filterData['days'] = $data['days'];
|
||||
$filterData['count_type'] = $data['count_type'];
|
||||
$filterData['count'] = $data['count'];
|
||||
$filterData['rating'] = $data['rating'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['total_spent_type'])
|
||||
|| !isset($data['total_spent_amount']) || $data['total_spent_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_TOTAL_SPENT_FIELDS);
|
||||
}
|
||||
$filterData['total_spent_type'] = $data['total_spent_type'];
|
||||
$filterData['total_spent_amount'] = $data['total_spent_amount'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['single_order_value_type'])
|
||||
|| !isset($data['single_order_value_amount']) || $data['single_order_value_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_SINGLE_ORDER_VALUE_FIELDS);
|
||||
}
|
||||
$filterData['single_order_value_type'] = $data['single_order_value_type'];
|
||||
$filterData['single_order_value_amount'] = $data['single_order_value_amount'];
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif (in_array($data['action'], [WooCommercePurchaseDate::ACTION, WooCommerceFirstOrder::ACTION])) {
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['value'] = $data['value'];
|
||||
} elseif ($data['action'] === WooCommerceAverageSpent::ACTION) {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
if (
|
||||
!isset($data['average_spent_type'])
|
||||
|| !isset($data['average_spent_amount']) || $data['average_spent_amount'] < 0
|
||||
) {
|
||||
throw new InvalidFilterException('Missing required fields', InvalidFilterException::MISSING_AVERAGE_SPENT_FIELDS);
|
||||
}
|
||||
$filterData['days'] = $data['days'] ?? 0;
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
$filterData['average_spent_amount'] = $data['average_spent_amount'];
|
||||
$filterData['average_spent_type'] = $data['average_spent_type'];
|
||||
} elseif ($data['action'] === WooCommerceUsedPaymentMethod::ACTION) {
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedPaymentMethod::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!isset($data['payment_methods']) || !is_array($data['payment_methods']) || empty($data['payment_methods'])) {
|
||||
throw new InvalidFilterException('Missing payment gateways', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['payment_methods'] = $data['payment_methods'];
|
||||
$filterData['days'] = intval($data['days'] ?? 0);
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommerceUsedShippingMethod::ACTION) {
|
||||
if (!isset($data['operator']) || !in_array($data['operator'], WooCommerceUsedShippingMethod::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!isset($data['shipping_methods']) || !is_array($data['shipping_methods']) || empty($data['shipping_methods'])) {
|
||||
throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['shipping_methods'] = $data['shipping_methods'];
|
||||
$filterData['days'] = intval($data['days'] ?? 0);
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif (in_array($data['action'], WooCommerceCustomerTextField::ACTIONS)) {
|
||||
if (empty($data['value'])) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (empty($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
if (!in_array($data['operator'], DynamicSegmentFilterData::TEXT_FIELD_OPERATORS)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['value'] = $data['value'];
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['action'] = $data['action'];
|
||||
} elseif ($data['action'] === WooCommerceUsedCouponCode::ACTION) {
|
||||
$this->wooCommerceUsedCouponCode->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['coupon_code_ids'] = $data['coupon_code_ids'];
|
||||
$filterData['days'] = $data['days'];
|
||||
$filterData['timeframe'] = $data['timeframe'];
|
||||
} elseif ($data['action'] === WooCommercePurchasedWithAttribute::ACTION) {
|
||||
$this->wooCommercePurchasedWithAttribute->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['attribute_taxonomy_slug'] = $data['attribute_taxonomy_slug'] ?? null;
|
||||
$filterData['attribute_term_ids'] = $data['attribute_term_ids'] ?? null;
|
||||
$filterData['attribute_type'] = $data['attribute_type'];
|
||||
$filterData['attribute_local_name'] = $data['attribute_local_name'] ?? null;
|
||||
$filterData['attribute_local_values'] = $data['attribute_local_values'] ?? null;
|
||||
} elseif ($data['action'] === WooCommerceTag::ACTION) {
|
||||
$this->wooCommerceTag->validateFilterData($data);
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['tag_ids'] = $data['tag_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerceMembership(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceMembership::ACTION_MEMBER_OF) {
|
||||
if (!isset($data['plan_ids']) || !is_array($data['plan_ids'])) {
|
||||
throw new InvalidFilterException('Missing plan', InvalidFilterException::MISSING_PLAN_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['plan_ids'] = $data['plan_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFilterException
|
||||
*/
|
||||
private function createWooCommerceSubscription(array $data): DynamicSegmentFilterData {
|
||||
if (empty($data['action'])) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
$filterData = [
|
||||
'connect' => $data['connect'],
|
||||
];
|
||||
$filterType = DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION;
|
||||
$action = $data['action'];
|
||||
if ($data['action'] === WooCommerceSubscription::ACTION_HAS_ACTIVE) {
|
||||
if (!isset($data['product_ids']) || !is_array($data['product_ids'])) {
|
||||
throw new InvalidFilterException('Missing product', InvalidFilterException::MISSING_PRODUCT_ID);
|
||||
}
|
||||
if (!isset($data['operator'])) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$filterData['operator'] = $data['operator'];
|
||||
$filterData['product_ids'] = $data['product_ids'];
|
||||
} else {
|
||||
throw new InvalidFilterException("Unknown action " . $data['action'], InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
return new DynamicSegmentFilterData($filterType, $action, $filterData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\AutomationsEvents;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailActionClickAny;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailOpensAbsoluteCountAction;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\EmailsReceived;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\Filter;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\MailPoetCustomFields;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\NumberOfClicks;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberDateField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberScore;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSegment;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberSubscribedViaForm;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\SubscriberTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\UserRole;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceAverageSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCategory;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCountry;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceCustomerTextField;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceFirstOrder;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceMembership;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfOrders;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceNumberOfReviews;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceProduct;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchaseDate;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommercePurchasedWithAttribute;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSingleOrderValue;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceSubscription;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTag;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceTotalSpent;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedCouponCode;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedPaymentMethod;
|
||||
use MailPoet\Segments\DynamicSegments\Filters\WooCommerceUsedShippingMethod;
|
||||
|
||||
class FilterFactory {
|
||||
/** @var EmailAction */
|
||||
private $emailAction;
|
||||
|
||||
/** @var UserRole */
|
||||
private $userRole;
|
||||
|
||||
/** @var WooCommerceAverageSpent */
|
||||
private $wooCommerceAverageSpent;
|
||||
|
||||
/** @var WooCommerceProduct */
|
||||
private $wooCommerceProduct;
|
||||
|
||||
/** @var WooCommerceCategory */
|
||||
private $wooCommerceCategory;
|
||||
|
||||
/** @var WooCommerceCountry */
|
||||
private $wooCommerceCountry;
|
||||
|
||||
/** @var WooCommerceNumberOfOrders */
|
||||
private $wooCommerceNumberOfOrders;
|
||||
|
||||
/** @var WooCommercePurchaseDate */
|
||||
private $wooCommercePurchaseDate;
|
||||
|
||||
/** @var WooCommerceSingleOrderValue */
|
||||
private $wooCommerceSingleOrderValue;
|
||||
|
||||
/** @var WooCommerceTotalSpent */
|
||||
private $wooCommerceTotalSpent;
|
||||
|
||||
/** @var WooCommerceMembership */
|
||||
private $wooCommerceMembership;
|
||||
|
||||
/** @var WooCommerceSubscription */
|
||||
private $wooCommerceSubscription;
|
||||
|
||||
/** @var EmailOpensAbsoluteCountAction */
|
||||
private $emailOpensAbsoluteCount;
|
||||
|
||||
/** @var SubscriberScore */
|
||||
private $subscriberScore;
|
||||
|
||||
/** @var MailPoetCustomFields */
|
||||
private $mailPoetCustomFields;
|
||||
|
||||
/** @var SubscriberSegment */
|
||||
private $subscriberSegment;
|
||||
|
||||
/** @var SubscriberTag */
|
||||
private $subscriberTag;
|
||||
|
||||
/** @var EmailActionClickAny */
|
||||
private $emailActionClickAny;
|
||||
|
||||
/** @var SubscriberSubscribedViaForm */
|
||||
private $subscribedViaForm;
|
||||
|
||||
/** @var SubscriberTextField */
|
||||
private $subscriberTextField;
|
||||
|
||||
/** @var WooCommerceUsedPaymentMethod */
|
||||
private $wooCommerceUsedPaymentMethod;
|
||||
|
||||
/** @var WooCommerceUsedShippingMethod */
|
||||
private $wooCommerceUsedShippingMethod;
|
||||
|
||||
/** @var WooCommerceCustomerTextField */
|
||||
private $wooCommerceCustomerTextField;
|
||||
|
||||
/** @var SubscriberDateField */
|
||||
private $subscriberDateField;
|
||||
|
||||
/** @var AutomationsEvents */
|
||||
private $automationsEvents;
|
||||
|
||||
/** @var WooCommerceNumberOfReviews */
|
||||
private $wooCommerceNumberOfReviews;
|
||||
|
||||
/** @var WooCommerceUsedCouponCode */
|
||||
private $wooCommerceUsedCouponCode;
|
||||
|
||||
/** @var WooCommerceFirstOrder */
|
||||
private $wooCommerceFirstOrder;
|
||||
|
||||
/** @var EmailsReceived */
|
||||
private $emailsReceived;
|
||||
|
||||
/** @var NumberOfClicks */
|
||||
private $numberOfClicks;
|
||||
|
||||
private WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute;
|
||||
|
||||
private WooCommerceTag $wooCommerceTag;
|
||||
|
||||
public function __construct(
|
||||
EmailAction $emailAction,
|
||||
EmailActionClickAny $emailActionClickAny,
|
||||
UserRole $userRole,
|
||||
MailPoetCustomFields $mailPoetCustomFields,
|
||||
WooCommerceProduct $wooCommerceProduct,
|
||||
WooCommerceCategory $wooCommerceCategory,
|
||||
WooCommerceCountry $wooCommerceCountry,
|
||||
WooCommerceCustomerTextField $wooCommerceCustomerTextField,
|
||||
EmailOpensAbsoluteCountAction $emailOpensAbsoluteCount,
|
||||
WooCommerceNumberOfOrders $wooCommerceNumberOfOrders,
|
||||
WooCommerceNumberOfReviews $wooCommerceNumberOfReviews,
|
||||
WooCommerceTotalSpent $wooCommerceTotalSpent,
|
||||
WooCommerceMembership $wooCommerceMembership,
|
||||
WooCommerceFirstOrder $wooCommerceFirstOrder,
|
||||
WooCommercePurchaseDate $wooCommercePurchaseDate,
|
||||
WooCommerceSubscription $wooCommerceSubscription,
|
||||
SubscriberScore $subscriberScore,
|
||||
SubscriberTag $subscriberTag,
|
||||
SubscriberSegment $subscriberSegment,
|
||||
SubscriberSubscribedViaForm $subscribedViaForm,
|
||||
WooCommerceSingleOrderValue $wooCommerceSingleOrderValue,
|
||||
WooCommerceAverageSpent $wooCommerceAverageSpent,
|
||||
WooCommerceTag $wooCommerceTag,
|
||||
WooCommerceUsedCouponCode $wooCommerceUsedCouponCode,
|
||||
WooCommerceUsedPaymentMethod $wooCommerceUsedPaymentMethod,
|
||||
WooCommerceUsedShippingMethod $wooCommerceUsedShippingMethod,
|
||||
SubscriberTextField $subscriberTextField,
|
||||
SubscriberDateField $subscriberDateField,
|
||||
AutomationsEvents $automationsEvents,
|
||||
EmailsReceived $emailsReceived,
|
||||
NumberOfClicks $numberOfClicks,
|
||||
WooCommercePurchasedWithAttribute $wooCommercePurchasedWithAttribute
|
||||
) {
|
||||
$this->emailAction = $emailAction;
|
||||
$this->userRole = $userRole;
|
||||
$this->wooCommerceProduct = $wooCommerceProduct;
|
||||
$this->wooCommerceCategory = $wooCommerceCategory;
|
||||
$this->wooCommerceCountry = $wooCommerceCountry;
|
||||
$this->wooCommerceNumberOfOrders = $wooCommerceNumberOfOrders;
|
||||
$this->wooCommerceNumberOfReviews = $wooCommerceNumberOfReviews;
|
||||
$this->wooCommerceMembership = $wooCommerceMembership;
|
||||
$this->wooCommercePurchaseDate = $wooCommercePurchaseDate;
|
||||
$this->wooCommerceSubscription = $wooCommerceSubscription;
|
||||
$this->emailOpensAbsoluteCount = $emailOpensAbsoluteCount;
|
||||
$this->wooCommerceTotalSpent = $wooCommerceTotalSpent;
|
||||
$this->subscriberScore = $subscriberScore;
|
||||
$this->subscriberTag = $subscriberTag;
|
||||
$this->mailPoetCustomFields = $mailPoetCustomFields;
|
||||
$this->subscriberSegment = $subscriberSegment;
|
||||
$this->emailActionClickAny = $emailActionClickAny;
|
||||
$this->wooCommerceSingleOrderValue = $wooCommerceSingleOrderValue;
|
||||
$this->subscriberTextField = $subscriberTextField;
|
||||
$this->subscribedViaForm = $subscribedViaForm;
|
||||
$this->wooCommerceAverageSpent = $wooCommerceAverageSpent;
|
||||
$this->wooCommerceUsedPaymentMethod = $wooCommerceUsedPaymentMethod;
|
||||
$this->wooCommerceUsedShippingMethod = $wooCommerceUsedShippingMethod;
|
||||
$this->wooCommerceCustomerTextField = $wooCommerceCustomerTextField;
|
||||
$this->automationsEvents = $automationsEvents;
|
||||
$this->subscriberDateField = $subscriberDateField;
|
||||
$this->wooCommerceUsedCouponCode = $wooCommerceUsedCouponCode;
|
||||
$this->wooCommerceFirstOrder = $wooCommerceFirstOrder;
|
||||
$this->emailsReceived = $emailsReceived;
|
||||
$this->numberOfClicks = $numberOfClicks;
|
||||
$this->wooCommercePurchasedWithAttribute = $wooCommercePurchasedWithAttribute;
|
||||
$this->wooCommerceTag = $wooCommerceTag;
|
||||
}
|
||||
|
||||
public function getFilterForFilterEntity(DynamicSegmentFilterEntity $filter): Filter {
|
||||
$filterData = $filter->getFilterData();
|
||||
$filterType = $filterData->getFilterType();
|
||||
$action = $filterData->getAction();
|
||||
switch ($filterType) {
|
||||
case DynamicSegmentFilterData::TYPE_AUTOMATIONS:
|
||||
return $this->automationsEvents;
|
||||
case DynamicSegmentFilterData::TYPE_USER_ROLE:
|
||||
return $this->userRole($action);
|
||||
case DynamicSegmentFilterData::TYPE_EMAIL:
|
||||
return $this->email($action);
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_MEMBERSHIP:
|
||||
return $this->wooCommerceMembership();
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE_SUBSCRIPTION:
|
||||
return $this->wooCommerceSubscription();
|
||||
case DynamicSegmentFilterData::TYPE_WOOCOMMERCE:
|
||||
return $this->wooCommerce($action);
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
*
|
||||
* @return MailPoetCustomFields|SubscriberScore|SubscriberSegment|UserRole|SubscriberTag|SubscriberTextField|SubscriberSubscribedViaForm|SubscriberDateField
|
||||
*/
|
||||
private function userRole(?string $action) {
|
||||
if ($action === SubscriberScore::TYPE) {
|
||||
return $this->subscriberScore;
|
||||
} elseif ($action === MailPoetCustomFields::TYPE) {
|
||||
return $this->mailPoetCustomFields;
|
||||
} elseif ($action === SubscriberSegment::TYPE) {
|
||||
return $this->subscriberSegment;
|
||||
} elseif ($action === SubscriberTag::TYPE) {
|
||||
return $this->subscriberTag;
|
||||
} elseif ($action === SubscriberSubscribedViaForm::TYPE) {
|
||||
return $this->subscribedViaForm;
|
||||
} elseif (in_array($action, SubscriberTextField::TYPES)) {
|
||||
return $this->subscriberTextField;
|
||||
} elseif (in_array($action, SubscriberDateField::TYPES)) {
|
||||
return $this->subscriberDateField;
|
||||
}
|
||||
return $this->userRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
* @return EmailAction|EmailActionClickAny|EmailOpensAbsoluteCountAction|EmailsReceived|NumberOfClicks
|
||||
*/
|
||||
private function email(?string $action) {
|
||||
$countActions = [EmailOpensAbsoluteCountAction::TYPE, EmailOpensAbsoluteCountAction::MACHINE_TYPE];
|
||||
if (in_array($action, $countActions)) {
|
||||
return $this->emailOpensAbsoluteCount;
|
||||
} elseif ($action === EmailActionClickAny::TYPE) {
|
||||
return $this->emailActionClickAny;
|
||||
} elseif ($action === EmailsReceived::ACTION) {
|
||||
return $this->emailsReceived;
|
||||
} elseif ($action === NumberOfClicks::ACTION) {
|
||||
return $this->numberOfClicks;
|
||||
}
|
||||
return $this->emailAction;
|
||||
}
|
||||
|
||||
private function wooCommerceMembership(): WooCommerceMembership {
|
||||
return $this->wooCommerceMembership;
|
||||
}
|
||||
|
||||
private function wooCommerceSubscription(): WooCommerceSubscription {
|
||||
return $this->wooCommerceSubscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $action
|
||||
* @return Filter
|
||||
*/
|
||||
private function wooCommerce(?string $action) {
|
||||
if ($action === WooCommerceProduct::ACTION_PRODUCT) {
|
||||
return $this->wooCommerceProduct;
|
||||
} elseif (in_array($action, WooCommerceNumberOfOrders::ACTIONS)) {
|
||||
return $this->wooCommerceNumberOfOrders;
|
||||
} elseif ($action === WooCommerceTotalSpent::ACTION_TOTAL_SPENT) {
|
||||
return $this->wooCommerceTotalSpent;
|
||||
} elseif ($action === WooCommerceCountry::ACTION_CUSTOMER_COUNTRY) {
|
||||
return $this->wooCommerceCountry;
|
||||
} elseif ($action === WooCommerceSingleOrderValue::ACTION_SINGLE_ORDER_VALUE) {
|
||||
return $this->wooCommerceSingleOrderValue;
|
||||
} elseif ($action === WooCommercePurchaseDate::ACTION) {
|
||||
return $this->wooCommercePurchaseDate;
|
||||
} elseif ($action === WooCommerceAverageSpent::ACTION) {
|
||||
return $this->wooCommerceAverageSpent;
|
||||
} elseif ($action === WooCommerceUsedPaymentMethod::ACTION) {
|
||||
return $this->wooCommerceUsedPaymentMethod;
|
||||
} elseif ($action === WooCommerceUsedShippingMethod::ACTION) {
|
||||
return $this->wooCommerceUsedShippingMethod;
|
||||
} elseif ($action === WooCommerceNumberOfReviews::ACTION) {
|
||||
return $this->wooCommerceNumberOfReviews;
|
||||
} elseif (in_array($action, WooCommerceCustomerTextField::ACTIONS)) {
|
||||
return $this->wooCommerceCustomerTextField;
|
||||
} elseif ($action === WooCommerceUsedCouponCode::ACTION) {
|
||||
return $this->wooCommerceUsedCouponCode;
|
||||
} elseif ($action === WooCommerceFirstOrder::ACTION) {
|
||||
return $this->wooCommerceFirstOrder;
|
||||
} elseif ($action === WooCommercePurchasedWithAttribute::ACTION) {
|
||||
return $this->wooCommercePurchasedWithAttribute;
|
||||
} elseif ($action == WooCommerceTag::ACTION) {
|
||||
return $this->wooCommerceTag;
|
||||
}
|
||||
return $this->wooCommerceCategory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\SegmentDependencyValidator;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class FilterHandler {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var SegmentDependencyValidator */
|
||||
private $segmentDependencyValidator;
|
||||
|
||||
/** @var FilterFactory */
|
||||
private $filterFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
SegmentDependencyValidator $segmentDependencyValidator,
|
||||
FilterFactory $filterFactory
|
||||
) {
|
||||
|
||||
$this->entityManager = $entityManager;
|
||||
$this->segmentDependencyValidator = $segmentDependencyValidator;
|
||||
$this->filterFactory = $filterFactory;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, SegmentEntity $segment): QueryBuilder {
|
||||
$filters = $segment->getDynamicFilters();
|
||||
$filterSelects = [];
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$pluginsForAllFiltersMissing = $this->segmentDependencyValidator->getMissingPluginsByAllFilters($filters);
|
||||
foreach ($filters as $filter) {
|
||||
$subscribersIdsQuery = $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select("DISTINCT $subscribersTable.id as inner_subscriber_id")
|
||||
->from($subscribersTable);
|
||||
// When a required plugin is missing we want to return empty result
|
||||
if ($pluginsForAllFiltersMissing || $this->segmentDependencyValidator->getMissingPluginsByFilter($filter)) {
|
||||
$subscribersIdsQuery->andWhere('1 = 0');
|
||||
} else {
|
||||
$this->filterFactory->getFilterForFilterEntity($filter)->apply($subscribersIdsQuery, $filter);
|
||||
}
|
||||
$filterSelects[] = $subscribersIdsQuery->getSQL();
|
||||
$queryBuilder->setParameters(
|
||||
array_merge(
|
||||
$subscribersIdsQuery->getParameters(),
|
||||
$queryBuilder->getParameters()
|
||||
),
|
||||
array_merge(
|
||||
$subscribersIdsQuery->getParameterTypes(),
|
||||
$queryBuilder->getParameterTypes()
|
||||
)
|
||||
);
|
||||
}
|
||||
$this->joinSubqueries($queryBuilder, $segment, $filterSelects);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function joinSubqueries(QueryBuilder $queryBuilder, SegmentEntity $segment, array $subQueries): QueryBuilder {
|
||||
$filter = $segment->getDynamicFilters()->first();
|
||||
if (!$filter) return $queryBuilder;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
if ($segment->getFiltersConnectOperator() === DynamicSegmentFilterData::CONNECT_TYPE_OR) {
|
||||
// the final query: SELECT * FROM subscribers INNER JOIN (filter_select1 UNION filter_select2) filtered_subscribers ON filtered_subscribers.inner_subscriber_id = id
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf('(%s)', join(' UNION ', $subQueries)),
|
||||
'filtered_subscribers',
|
||||
"filtered_subscribers.inner_subscriber_id = $subscribersTable.id"
|
||||
);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
foreach ($subQueries as $key => $subQuery) {
|
||||
// we need a unique name for each subquery so that we can join them together in the sql query - just make sure the identifier starts with a letter, not a number
|
||||
$subqueryName = 'a' . $key;
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
"($subQuery)",
|
||||
$subqueryName,
|
||||
"$subqueryName.inner_subscriber_id = $subscribersTable.id"
|
||||
);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Automation\Engine\Data\Automation;
|
||||
use MailPoet\Automation\Engine\Data\AutomationRun;
|
||||
use MailPoet\Automation\Engine\Storage\AutomationStorage;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class AutomationsEvents implements Filter {
|
||||
|
||||
const SUPPORTED_ACTIONS = [
|
||||
self::ENTERED_ACTION,
|
||||
self::EXITED_ACTION,
|
||||
];
|
||||
|
||||
const ENTERED_ACTION = 'enteredAutomation';
|
||||
const EXITED_ACTION = 'exitedAutomation';
|
||||
const AUTOMATION_IDS_PARAM = 'automation_ids';
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var AutomationStorage */
|
||||
private $automationStorage;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
AutomationStorage $automationStorage
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->automationStorage = $automationStorage;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$automationIds = $filterData->getParam(self::AUTOMATION_IDS_PARAM);
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $action, $automationIds);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $action, $automationIds);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $action, $automationIds);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, $action, $automationIds) {
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$automationsTable = $this->filterHelper->getPrefixedTable('mailpoet_automations');
|
||||
$automationRunsTable = $this->filterHelper->getPrefixedTable('mailpoet_automation_runs');
|
||||
$automationRunSubjectsTable = $this->filterHelper->getPrefixedTable('mailpoet_automation_run_subjects');
|
||||
$automationIdsParam = $this->filterHelper->getUniqueParameterName('automationIds');
|
||||
|
||||
$queryBuilder
|
||||
->innerJoin(
|
||||
$subscribersTable,
|
||||
$automationRunSubjectsTable,
|
||||
'subjects',
|
||||
"subjects.key = 'mailpoet:subscriber' AND subjects.args = CONCAT('{\"subscriber_id\":', $subscribersTable.id, '}')"
|
||||
)
|
||||
->innerJoin(
|
||||
'subjects',
|
||||
$automationRunsTable,
|
||||
'runs',
|
||||
'subjects.automation_run_id = runs.id'
|
||||
)
|
||||
->innerJoin(
|
||||
'runs',
|
||||
$automationsTable,
|
||||
'automations',
|
||||
'automations.id = runs.automation_id'
|
||||
)
|
||||
->andWhere("automations.id IN (:$automationIdsParam)")
|
||||
->setParameter($automationIdsParam, $automationIds, ArrayParameterType::STRING);
|
||||
|
||||
if ($action === self::EXITED_ACTION) {
|
||||
$statusParam = $this->filterHelper->getUniqueParameterName('status');
|
||||
$queryBuilder
|
||||
->andWhere("runs.status != :$statusParam")
|
||||
->setParameter($statusParam, AutomationRun::STATUS_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, $action, $automationIds) {
|
||||
$this->applyForAnyOperator($queryBuilder, $action, $automationIds);
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT automations.id) = " . count($automationIds));
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$automationIds = $filterData->getArrayParam(self::AUTOMATION_IDS_PARAM);
|
||||
$lookupData = [
|
||||
'automations' => [],
|
||||
];
|
||||
|
||||
foreach ($automationIds as $automationId) {
|
||||
$automation = $this->automationStorage->getAutomation(intval($automationId));
|
||||
if ($automation instanceof Automation) {
|
||||
$lookupData['automations'][$automationId] = $automation->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
|
||||
class DateFilterHelper {
|
||||
const BEFORE = 'before';
|
||||
const AFTER = 'after';
|
||||
const ON = 'on';
|
||||
const ON_OR_BEFORE = 'onOrBefore';
|
||||
const ON_OR_AFTER = 'onOrAfter';
|
||||
const NOT_ON = 'notOn';
|
||||
const IN_THE_LAST = 'inTheLast';
|
||||
const NOT_IN_THE_LAST = 'notInTheLast';
|
||||
|
||||
public function getValidOperators(): array {
|
||||
return array_merge(
|
||||
$this->getAbsoluteDateOperators(),
|
||||
$this->getRelativeDateOperators()
|
||||
);
|
||||
}
|
||||
|
||||
public function getAbsoluteDateOperators(): array {
|
||||
return [
|
||||
self::BEFORE,
|
||||
self::AFTER,
|
||||
self::ON,
|
||||
self::ON_OR_BEFORE,
|
||||
self::ON_OR_AFTER,
|
||||
self::NOT_ON,
|
||||
];
|
||||
}
|
||||
|
||||
public function getRelativeDateOperators(): array {
|
||||
return [
|
||||
self::IN_THE_LAST,
|
||||
self::NOT_IN_THE_LAST,
|
||||
];
|
||||
}
|
||||
|
||||
public function getDateStringForOperator(string $operator, string $value): string {
|
||||
if (in_array($operator, self::getAbsoluteDateOperators())) {
|
||||
$carbon = CarbonImmutable::createFromFormat('Y-m-d', $value);
|
||||
if (!$carbon instanceof CarbonImmutable) {
|
||||
throw new InvalidFilterException('Invalid date value', InvalidFilterException::INVALID_DATE_VALUE);
|
||||
}
|
||||
} else if (in_array($operator, self::getRelativeDateOperators())) {
|
||||
$carbon = CarbonImmutable::now()->subDays(intval($value) - 1);
|
||||
} else {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
return $carbon->toDateString();
|
||||
}
|
||||
|
||||
public function getDateValueFromFilter(DynamicSegmentFilterEntity $filter): string {
|
||||
$filterData = $filter->getFilterData();
|
||||
$dateValue = $filterData->getParam('value');
|
||||
if (!is_string($dateValue)) {
|
||||
throw new InvalidFilterException('Incorrect value for date', InvalidFilterException::INVALID_DATE_VALUE);
|
||||
}
|
||||
return $dateValue;
|
||||
}
|
||||
|
||||
public function getOperatorFromFilter(DynamicSegmentFilterEntity $filter): string {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!is_string($operator) || !in_array($operator, $this->getValidOperators())) {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
return $operator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\StatisticsNewsletterEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Newsletter\NewslettersRepository;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailAction implements Filter {
|
||||
const ACTION_OPENED = 'opened';
|
||||
const ACTION_MACHINE_OPENED = 'machineOpened';
|
||||
/** @deprecated */
|
||||
const ACTION_NOT_OPENED = 'notOpened';
|
||||
const ACTION_CLICKED = 'clicked';
|
||||
const ACTION_WAS_SENT = 'wasSent';
|
||||
/** @deprecated */
|
||||
const ACTION_NOT_CLICKED = 'notClicked';
|
||||
|
||||
const ALLOWED_ACTIONS = [
|
||||
self::ACTION_OPENED,
|
||||
self::ACTION_MACHINE_OPENED,
|
||||
self::ACTION_CLICKED,
|
||||
self::ACTION_WAS_SENT,
|
||||
EmailActionClickAny::TYPE,
|
||||
EmailOpensAbsoluteCountAction::TYPE,
|
||||
EmailOpensAbsoluteCountAction::MACHINE_TYPE,
|
||||
EmailsReceived::ACTION,
|
||||
NumberOfClicks::ACTION,
|
||||
];
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var NewslettersRepository */
|
||||
private $newslettersRepository;
|
||||
|
||||
/** @var NewsletterLinkRepository */
|
||||
private $newsletterLinkRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
NewslettersRepository $newslettersRepository,
|
||||
NewsletterLinkRepository $newsletterLinkRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->newslettersRepository = $newslettersRepository;
|
||||
$this->newsletterLinkRepository = $newsletterLinkRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getAction();
|
||||
$parameterSuffix = (string)($filter->getId() ?? Security::generateRandomString());
|
||||
|
||||
if ($action === self::ACTION_CLICKED) {
|
||||
return $this->applyForClickedActions($queryBuilder, $filterData, $parameterSuffix);
|
||||
} elseif ($action === self::ACTION_WAS_SENT) {
|
||||
return $this->applyForWasSentAction($queryBuilder, $filterData, $parameterSuffix);
|
||||
} else {
|
||||
return $this->applyForOpenedActions($queryBuilder, $filterData, $parameterSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForClickedActions(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$action = $filterData->getAction();
|
||||
$newsletterId = $filterData->getParam('newsletter_id');
|
||||
$linkIds = $filterData->getParam('link_ids');
|
||||
if (!is_array($linkIds)) {
|
||||
$linkIds = [];
|
||||
}
|
||||
|
||||
$statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
|
||||
$where = '1';
|
||||
|
||||
if (($action === self::ACTION_NOT_CLICKED) || ($operator === DynamicSegmentFilterData::OPERATOR_NONE)) {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsSentTable,
|
||||
'statssent',
|
||||
"$subscribersTable.id = statssent.subscriber_id AND statssent.newsletter_id = :newsletter" . $parameterSuffix
|
||||
)->leftJoin(
|
||||
'statssent',
|
||||
$statsTable,
|
||||
'stats',
|
||||
$this->createNotStatsJoinCondition($parameterSuffix, $linkIds)
|
||||
)->setParameter('newsletter' . $parameterSuffix, $newsletterId);
|
||||
$where .= ' AND stats.id IS NULL';
|
||||
} else {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id AND stats.newsletter_id = :newsletter" . $parameterSuffix
|
||||
)->setParameter('newsletter' . $parameterSuffix, $newsletterId);
|
||||
}
|
||||
|
||||
if ($action === EmailAction::ACTION_CLICKED && $operator !== DynamicSegmentFilterData::OPERATOR_NONE && $linkIds) {
|
||||
$where .= ' AND stats.link_id IN (:links' . $parameterSuffix . ')';
|
||||
}
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
if ($linkIds) {
|
||||
$queryBuilder->having('COUNT(1) = ' . count($linkIds));
|
||||
} else {
|
||||
// Case when a user selects all of, but doesn't specify links == all of all links.
|
||||
$linksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
$linksQueryBuilder = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$linkCount = $linksQueryBuilder->select('count(id)')
|
||||
->from($linksTable)
|
||||
->where('newsletter_id = :newsletter_id')
|
||||
->setParameter('newsletter_id', $newsletterId)
|
||||
->execute()
|
||||
->fetchOne();
|
||||
$queryBuilder->having('COUNT(1) = ' . $linkCount);
|
||||
}
|
||||
}
|
||||
$queryBuilder = $queryBuilder->andWhere($where);
|
||||
if ($linkIds) {
|
||||
$queryBuilder = $queryBuilder
|
||||
->setParameter('links' . $parameterSuffix, $linkIds, ArrayParameterType::STRING);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForOpenedActions(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$action = $filterData->getAction();
|
||||
$newsletters = $filterData->getParam('newsletters');
|
||||
|
||||
$statsSentTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
|
||||
|
||||
$where = '1';
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsSentTable,
|
||||
'statssent',
|
||||
"$subscribersTable.id = statssent.subscriber_id AND statssent.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->leftJoin(
|
||||
'statssent',
|
||||
$statsTable,
|
||||
'stats',
|
||||
"statssent.subscriber_id = stats.subscriber_id AND stats.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
$where .= ' AND stats.id IS NULL';
|
||||
} else {
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id AND stats.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($newsletters));
|
||||
}
|
||||
}
|
||||
if (($action === EmailAction::ACTION_OPENED) && ($operator !== DynamicSegmentFilterData::OPERATOR_NONE)) {
|
||||
$queryBuilder->andWhere('stats.user_agent_type = :userAgentType')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
|
||||
}
|
||||
if ($action === EmailAction::ACTION_MACHINE_OPENED) {
|
||||
$queryBuilder->andWhere('(stats.user_agent_type = :userAgentType)')
|
||||
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE);
|
||||
}
|
||||
$queryBuilder = $queryBuilder->andWhere($where);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function createNotStatsJoinCondition(string $parameterSuffix, array $linkIds = null): string {
|
||||
$clause = "statssent.subscriber_id = stats.subscriber_id AND stats.newsletter_id = :newsletter" . $parameterSuffix;
|
||||
if ($linkIds) {
|
||||
$clause .= ' AND stats.link_id IN (:links' . $parameterSuffix . ')';
|
||||
}
|
||||
return $clause;
|
||||
}
|
||||
|
||||
private function applyForWasSentAction(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData, string $parameterSuffix): QueryBuilder {
|
||||
$newsletters = (array)$filterData->getParam('newsletters');
|
||||
$operator = $filterData->getParam('operator') ?? DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$statisticsNewslettersTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->leftJoin(
|
||||
$this->filterHelper->getSubscribersTable(),
|
||||
$statisticsNewslettersTable,
|
||||
'statisticsNewsletter',
|
||||
"$subscribersTable.id = statisticsNewsletter.subscriber_id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)
|
||||
->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER)
|
||||
->andWhere('statisticsNewsletter.subscriber_id IS NULL');
|
||||
} else {
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statisticsNewslettersTable,
|
||||
'statisticsNewsletter',
|
||||
"statisticsNewsletter.subscriber_id = $subscribersTable.id AND statisticsNewsletter.newsletter_id IN (:newsletters" . $parameterSuffix . ')'
|
||||
)->setParameter('newsletters' . $parameterSuffix, $newsletters, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($newsletters));
|
||||
}
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'newsletters' => [],
|
||||
'links' => [],
|
||||
];
|
||||
$newsletterIds = $filterData->getParam('newsletters');
|
||||
if (!is_array($newsletterIds)) {
|
||||
$newsletterIds = [];
|
||||
}
|
||||
|
||||
// Clicked action only supports single newsletter ID
|
||||
$singularNewsletterId = $filterData->getParam('newsletter_id');
|
||||
if (!is_null($singularNewsletterId)) {
|
||||
$newsletterIds[] = $singularNewsletterId;
|
||||
}
|
||||
|
||||
$linkIds = $filterData->getParam('link_ids');
|
||||
if (!is_array($linkIds)) {
|
||||
$linkIds = [];
|
||||
}
|
||||
|
||||
foreach ($newsletterIds as $newsletterId) {
|
||||
$newsletter = $this->newslettersRepository->findOneById($newsletterId);
|
||||
if ($newsletter instanceof NewsletterEntity) {
|
||||
$lookupData['newsletters'][$newsletterId] = $newsletter->getSubject();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($linkIds as $linkId) {
|
||||
$link = $this->newsletterLinkRepository->findOneById($linkId);
|
||||
if ($link instanceof NewsletterLinkEntity) {
|
||||
$lookupData['links'][$linkId] = $link->getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailActionClickAny implements Filter {
|
||||
const TYPE = 'clickedAny';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$newsletterLinksTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName();
|
||||
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
|
||||
$excludedLinks = [
|
||||
'[link:subscription_unsubscribe_url]',
|
||||
'[link:subscription_instant_unsubscribe_url]',
|
||||
'[link:newsletter_view_in_browser_url]',
|
||||
'[link:subscription_manage_url]',
|
||||
];
|
||||
$queryBuilder = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'stats',
|
||||
"stats.subscriber_id = $subscribersTable.id"
|
||||
)->innerJoin(
|
||||
'stats',
|
||||
$newsletterLinksTable,
|
||||
'newsletterLinks',
|
||||
"stats.link_id = newsletterLinks.id AND newsletterLinks.URL NOT IN ('" . join("', '", $excludedLinks) . "')"
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailOpensAbsoluteCountAction implements Filter {
|
||||
const TYPE = 'opensAbsoluteCount';
|
||||
const MACHINE_TYPE = 'machineOpensAbsoluteCount';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$days = $filterData->getParam('days');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$action = $filterData->getAction();
|
||||
$timeframe = $filterData->getParam('timeframe');
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'opens',
|
||||
"{$subscribersTable}.id = opens.subscriber_id AND opens.user_agent_type = :userAgentType{$parameterSuffix}"
|
||||
);
|
||||
} else {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$statsTable,
|
||||
'opens',
|
||||
"{$subscribersTable}.id = opens.subscriber_id AND opens.created_at > :newer{$parameterSuffix} AND opens.user_agent_type = :userAgentType{$parameterSuffix}"
|
||||
);
|
||||
$queryBuilder->setParameter('newer' . $parameterSuffix, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(opens.id) = :opens" . $parameterSuffix);
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(opens.id) != :opens" . $parameterSuffix);
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(opens.id) < :opens" . $parameterSuffix);
|
||||
} else {
|
||||
$queryBuilder->having("count(opens.id) > :opens" . $parameterSuffix);
|
||||
}
|
||||
$queryBuilder->setParameter('opens' . $parameterSuffix, $filterData->getParam('opens'));
|
||||
|
||||
if ($action === EmailOpensAbsoluteCountAction::TYPE) {
|
||||
$queryBuilder->setParameter('userAgentType' . $parameterSuffix, UserAgentEntity::USER_AGENT_TYPE_HUMAN);
|
||||
} else {
|
||||
$queryBuilder->setParameter('userAgentType' . $parameterSuffix, UserAgentEntity::USER_AGENT_TYPE_MACHINE);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsNewsletterEntity;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class EmailsReceived implements Filter {
|
||||
const ACTION = 'numberReceived';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$emailCount = $filterData->getIntParam('emails');
|
||||
$operator = $filterData->getStringParam('operator');
|
||||
$timeframe = $filterData->getStringParam('timeframe');
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName();
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'emails', "{$subscribersTable}.id = emails.subscriber_id");
|
||||
} else {
|
||||
$days = $filterData->getIntParam('days');
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('days');
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'emails', "{$subscribersTable}.id = emails.subscriber_id AND emails.sent_at >= :$dateParam");
|
||||
$queryBuilder->setParameter($dateParam, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
$emailCountParam = $this->filterHelper->getUniqueParameterName('emails');
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(emails.id) = :$emailCountParam");
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(emails.id) != :$emailCountParam");
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(emails.id) < :$emailCountParam");
|
||||
} else {
|
||||
$queryBuilder->having("count(emails.id) > :$emailCountParam");
|
||||
}
|
||||
$queryBuilder->setParameter($emailCountParam, $emailCount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
interface Filter {
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder;
|
||||
|
||||
/**
|
||||
* At sending time, we store the current state of every filter so we can tell in the future how it was configured. This
|
||||
* method should be used to return any data that might change after sending time. For example, if a filter stores IDs
|
||||
* of related entities, we should try to look up descriptive names for those entities in case they get deleted or
|
||||
* renamed later.
|
||||
*
|
||||
* @param DynamicSegmentFilterData $filterData
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class FilterHelper {
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function getPrefixedTable(string $table): string {
|
||||
global $wpdb;
|
||||
return sprintf('%s%s', $wpdb->prefix, $table);
|
||||
}
|
||||
|
||||
public function getNewSubscribersQueryBuilder(): QueryBuilder {
|
||||
return $this->entityManager
|
||||
->getConnection()
|
||||
->createQueryBuilder()
|
||||
->select($this->getSubscribersTable() . '.id')
|
||||
->from($this->getSubscribersTable());
|
||||
}
|
||||
|
||||
public function getSubscribersTable(): string {
|
||||
return $this->getTableForEntity(SubscriberEntity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<object> $entityClass
|
||||
*/
|
||||
public function getTableForEntity(string $entityClass): string {
|
||||
return $this->entityManager->getClassMetadata($entityClass)->getTableName();
|
||||
}
|
||||
|
||||
public function getInterpolatedSQL(QueryBuilder $query): string {
|
||||
$sql = $query->getSQL();
|
||||
$params = $query->getParameters();
|
||||
$search = array_map(function($key) {
|
||||
return ":$key";
|
||||
}, array_keys($params));
|
||||
$replace = array_map(function($value) use ($query) {
|
||||
if (is_array($value)) {
|
||||
$quotedValues = array_map(function($arrayValue) use ($query) {
|
||||
return $query->expr()->literal($arrayValue);
|
||||
}, $value);
|
||||
return implode(',', $quotedValues);
|
||||
}
|
||||
return $query->expr()->literal($value);
|
||||
}, array_values($params));
|
||||
return str_replace($search, $replace, $sql);
|
||||
}
|
||||
|
||||
public function getUniqueParameterName(string $parameter): string {
|
||||
$suffix = Security::generateRandomString();
|
||||
return sprintf("%s_%s", $parameter, $suffix);
|
||||
}
|
||||
|
||||
public function validateDaysPeriodData(array $data): void {
|
||||
if (!isset($data['timeframe']) || !in_array($data['timeframe'], [DynamicSegmentFilterData::TIMEFRAME_ALL_TIME, DynamicSegmentFilterData::TIMEFRAME_IN_THE_LAST], true)) {
|
||||
throw new InvalidFilterException('Missing timeframe type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if ($data['timeframe'] === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
$days = intval($data['days'] ?? null);
|
||||
|
||||
if ($days < 1) {
|
||||
throw new InvalidFilterException('Missing number of days', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\CustomFields\CustomFieldsRepository;
|
||||
use MailPoet\Entities\CustomFieldEntity;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberCustomFieldEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class MailPoetCustomFields implements Filter {
|
||||
const TYPE = 'mailpoetCustomField';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var CustomFieldsRepository */
|
||||
private $customFieldsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
CustomFieldsRepository $customFieldsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->customFieldsRepository = $customFieldsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$customFieldType = $filterData->getParam('custom_field_type');
|
||||
$customFieldId = $filterData->getParam('custom_field_id');
|
||||
$parameterSuffix = (string)($filter->getId() ?? Security::generateRandomString());
|
||||
$customFieldIdParam = 'customFieldId' . $parameterSuffix;
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscribersCustomFieldTable = $this->entityManager->getClassMetadata(SubscriberCustomFieldEntity::class)->getTableName();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$subscribersCustomFieldTable,
|
||||
'subscribers_custom_field',
|
||||
"$subscribersTable.id = subscribers_custom_field.subscriber_id AND subscribers_custom_field.custom_field_id = :$customFieldIdParam"
|
||||
);
|
||||
$queryBuilder->setParameter($customFieldIdParam, $customFieldId);
|
||||
|
||||
$valueParam = 'value' . $parameterSuffix;
|
||||
if (
|
||||
($customFieldType === CustomFieldEntity::TYPE_TEXT)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_TEXTAREA)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_RADIO)
|
||||
|| ($customFieldType === CustomFieldEntity::TYPE_SELECT)
|
||||
) {
|
||||
$queryBuilder = $this->applyEquality($queryBuilder, $filter, $valueParam);
|
||||
}
|
||||
if ($customFieldType === CustomFieldEntity::TYPE_CHECKBOX) {
|
||||
$queryBuilder = $this->applyForCheckbox($queryBuilder, $filter);
|
||||
}
|
||||
if ($customFieldType === CustomFieldEntity::TYPE_DATE) {
|
||||
$queryBuilder = $this->applyForDate($queryBuilder, $filter, $valueParam);
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDate(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter, string $valueParam): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$dateType = $filterData->getParam('date_type');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
if ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NULL');
|
||||
return $queryBuilder;
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NOT NULL');
|
||||
return $queryBuilder;
|
||||
} elseif ($dateType === 'month') {
|
||||
return $this->applyForDateMonth($queryBuilder, $valueParam);
|
||||
} elseif ($dateType === 'year') {
|
||||
return $this->applyForDateYear($queryBuilder, $operator, $valueParam);
|
||||
}
|
||||
return $this->applyForDateEqual($queryBuilder, $operator, $valueParam);
|
||||
}
|
||||
|
||||
private function applyForDateMonth(QueryBuilder $queryBuilder, string $valueParam): QueryBuilder {
|
||||
$queryBuilder->andWhere("month(subscribers_custom_field.value) = month(:$valueParam)");
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDateYear(QueryBuilder $queryBuilder, ?string $operator, string $valueParam): QueryBuilder {
|
||||
if ($operator === 'before') {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) < year(:$valueParam)");
|
||||
} elseif ($operator === 'after') {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) > year(:$valueParam)");
|
||||
} else {
|
||||
$queryBuilder->andWhere("year(subscribers_custom_field.value) = year(:$valueParam)");
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForDateEqual(QueryBuilder $queryBuilder, ?string $operator, string $valueParam): QueryBuilder {
|
||||
if ($operator === 'before') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value < :$valueParam");
|
||||
} elseif ($operator === 'after') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value > :$valueParam");
|
||||
} else {
|
||||
// we always save full date in the database: 2018-03-01 00:00:00
|
||||
// so this works even for year_month where we save the first day of the month
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value = :$valueParam");
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForCheckbox(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NULL');
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value IS NOT NULL');
|
||||
} elseif ($value === '1') {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value = 1');
|
||||
} elseif ($value === '0') {
|
||||
$queryBuilder->andWhere('subscribers_custom_field.value <> 1');
|
||||
}
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyEquality(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter, string $valueParam): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
|
||||
$operator = $filterData->getParam('operator');
|
||||
$value = $filterData->getParam('value');
|
||||
|
||||
$requiresValue = !in_array($operator, [DynamicSegmentFilterData::IS_BLANK, DynamicSegmentFilterData::IS_NOT_BLANK]);
|
||||
|
||||
if ($requiresValue && !is_string($value)) {
|
||||
throw new InvalidFilterException('Missing required value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
/** @var string $value - for PhpStan */
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value = :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'not_equals') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value != :$valueParam");
|
||||
$queryBuilder->orWhere('subscribers_custom_field.value IS NULL');
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'more_than') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value > :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === 'less_than') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value < :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, $value);
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_BLANK) {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value IS NULL OR subscribers_custom_field.value = ''");
|
||||
} elseif ($operator === DynamicSegmentFilterData::IS_NOT_BLANK) {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value IS NOT NULL AND subscribers_custom_field.value != ''");
|
||||
} elseif ($operator === 'not_contains') {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value NOT LIKE :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, '%' . Helpers::escapeSearch($value) . '%');
|
||||
} else {
|
||||
$queryBuilder->andWhere("subscribers_custom_field.value LIKE :$valueParam");
|
||||
$queryBuilder->setParameter($valueParam, '%' . Helpers::escapeSearch($value) . '%');
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'customFields' => [],
|
||||
];
|
||||
$customFieldId = $filterData->getIntParam('custom_field_id');
|
||||
$customField = $this->customFieldsRepository->findOneById($customFieldId);
|
||||
if ($customField instanceof CustomFieldEntity) {
|
||||
$lookupData['customFields'][$customFieldId] = $customField->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoetVendor\Carbon\CarbonImmutable;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class NumberOfClicks implements Filter {
|
||||
const ACTION = 'numberOfClicks';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$clickCount = $filterData->getIntParam('clicks');
|
||||
$operator = $filterData->getStringParam('operator');
|
||||
$timeframe = $filterData->getStringParam('timeframe');
|
||||
$statsTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName();
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if ($timeframe === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'clicks', "{$subscribersTable}.id = clicks.subscriber_id");
|
||||
} else {
|
||||
$days = $filterData->getIntParam('days');
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('days');
|
||||
$queryBuilder->leftJoin($subscribersTable, $statsTable, 'clicks', "{$subscribersTable}.id = clicks.subscriber_id AND clicks.created_at >= :$dateParam");
|
||||
$queryBuilder->setParameter($dateParam, CarbonImmutable::now()->subDays($days)->startOfDay());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
$clicksCountParam = $this->filterHelper->getUniqueParameterName('clicks');
|
||||
|
||||
if ($operator === 'equals') {
|
||||
$queryBuilder->having("count(clicks.id) = :$clicksCountParam");
|
||||
} else if ($operator === 'not_equals') {
|
||||
$queryBuilder->having("count(clicks.id) != :$clicksCountParam");
|
||||
} else if ($operator === 'less') {
|
||||
$queryBuilder->having("count(clicks.id) < :$clicksCountParam");
|
||||
} else {
|
||||
$queryBuilder->having("count(clicks.id) > :$clicksCountParam");
|
||||
}
|
||||
$queryBuilder->setParameter($clicksCountParam, $clickCount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberDateField implements Filter {
|
||||
const LAST_CLICK_DATE = 'lastClickDate';
|
||||
const LAST_ENGAGEMENT_DATE = 'lastEngagementDate';
|
||||
const LAST_PURCHASE_DATE = 'lastPurchaseDate';
|
||||
const LAST_OPEN_DATE = 'lastOpenDate';
|
||||
const LAST_PAGE_VIEW_DATE = 'lastPageViewDate';
|
||||
const LAST_SENDING_DATE = 'lastSendingDate';
|
||||
|
||||
// Slightly different naming due to backwards compatibility
|
||||
const SUBSCRIBED_DATE = 'subscribedDate';
|
||||
|
||||
const TYPES = [
|
||||
self::LAST_CLICK_DATE,
|
||||
self::LAST_ENGAGEMENT_DATE,
|
||||
self::LAST_PURCHASE_DATE,
|
||||
self::LAST_OPEN_DATE,
|
||||
self::LAST_PAGE_VIEW_DATE,
|
||||
self::LAST_SENDING_DATE,
|
||||
self::SUBSCRIBED_DATE,
|
||||
];
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
DateFilterHelper $dateFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$action = $filter->getFilterData()->getAction();
|
||||
$value = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('date');
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $value);
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
$columnName = $this->getColumnNameForAction($action);
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
$queryBuilder->andWhere("DATE($columnName) < :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andWhere("DATE($columnName) > :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
$queryBuilder->andWhere("DATE($columnName) = :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andWhere("DATE($columnName) <= :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andWhere("DATE($columnName) != :$parameter");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andWhere("DATE($columnName) >= :$parameter");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($parameter, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getColumnNameForAction(string $action): string {
|
||||
switch ($action) {
|
||||
case self::LAST_CLICK_DATE:
|
||||
return 'last_click_at';
|
||||
case self::LAST_ENGAGEMENT_DATE:
|
||||
return 'last_engagement_at';
|
||||
case self::LAST_PURCHASE_DATE:
|
||||
return 'last_purchase_at';
|
||||
case self::LAST_OPEN_DATE:
|
||||
return 'last_open_at';
|
||||
case self::LAST_PAGE_VIEW_DATE:
|
||||
return 'last_page_view_at';
|
||||
case self::SUBSCRIBED_DATE:
|
||||
return 'last_subscribed_at';
|
||||
case self::LAST_SENDING_DATE:
|
||||
return 'last_sending_at';
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberScore implements Filter {
|
||||
const TYPE = 'subscriberScore';
|
||||
|
||||
const HIGHER_THAN = 'higherThan';
|
||||
const LOWER_THAN = 'lowerThan';
|
||||
const EQUALS = 'equals';
|
||||
const NOT_EQUALS = 'not_equals';
|
||||
const UNKNOWN = 'unknown';
|
||||
const NOT_UNKNOWN = 'not_unknown';
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$parameter = 'score' . $parameterSuffix;
|
||||
|
||||
if ($operator === self::HIGHER_THAN) {
|
||||
$queryBuilder->andWhere("engagement_score > :$parameter");
|
||||
} elseif ($operator === self::LOWER_THAN) {
|
||||
$queryBuilder->andWhere("engagement_score < :$parameter");
|
||||
} elseif ($operator === self::EQUALS) {
|
||||
$queryBuilder->andWhere("engagement_score = :$parameter");
|
||||
} elseif ($operator === self::NOT_EQUALS) {
|
||||
$queryBuilder->andWhere("engagement_score != :$parameter");
|
||||
} elseif ($operator === self::UNKNOWN) {
|
||||
$queryBuilder->andWhere("engagement_score IS NULL");
|
||||
} elseif ($operator === self::NOT_UNKNOWN) {
|
||||
$queryBuilder->andWhere("engagement_score IS NOT NULL");
|
||||
} else {
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($parameter, (int)$value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\SubscriberSegmentEntity;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class SubscriberSegment implements Filter {
|
||||
const TYPE = 'subscribedToList';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
SegmentsRepository $segmentsRepository
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$segments = $filterData->getParam('segments');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$statusSubscribedParam = 'subscribed' . $parameterSuffix;
|
||||
$segmentsParam = 'segments' . $parameterSuffix;
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName();
|
||||
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$subscriberSegmentTable,
|
||||
'subscriber_segments',
|
||||
"$subscribersTable.id = subscriber_segments.subscriber_id"
|
||||
. ' AND subscriber_segments.status = :' . $statusSubscribedParam
|
||||
. ' AND subscriber_segments.segment_id IN (:' . $segmentsParam . ')'
|
||||
);
|
||||
|
||||
$queryBuilder->setParameter($statusSubscribedParam, SubscriberEntity::STATUS_SUBSCRIBED);
|
||||
$queryBuilder->setParameter($segmentsParam, $segments, ArrayParameterType::INTEGER);
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->andWhere('subscriber_segments.id IS NULL');
|
||||
} else {
|
||||
$queryBuilder->andWhere('subscriber_segments.id IS NOT NULL');
|
||||
}
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$queryBuilder->groupBy('subscriber_id');
|
||||
$queryBuilder->having('COUNT(1) = ' . count($segments));
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'segments' => [],
|
||||
];
|
||||
$segmentIds = $filterData->getArrayParam('segments');
|
||||
$segments = $this->segmentsRepository->findBy(['id' => $segmentIds]);
|
||||
foreach ($segments as $segment) {
|
||||
$lookupData['segments'][$segment->getId()] = $segment->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\StatisticsFormEntity;
|
||||
use MailPoet\Form\FormsRepository;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberSubscribedViaForm implements Filter {
|
||||
const TYPE = 'subscribedViaForm';
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var FormsRepository */
|
||||
private $formsRepository;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
FormsRepository $formsRepository
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->formsRepository = $formsRepository;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$formIds = $filterData->getParam('form_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$formStatsTable = $this->filterHelper->getTableForEntity(StatisticsFormEntity::class);
|
||||
|
||||
$formIdsParam = $this->filterHelper->getUniqueParameterName('formIds');
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$formStatsTable,
|
||||
'statisticsForms',
|
||||
"$subscribersTable.id = statisticsForms.subscriber_id"
|
||||
);
|
||||
$queryBuilder->andWhere("statisticsForms.form_id IN (:$formIdsParam)");
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$queryBuilder->leftJoin(
|
||||
$subscribersTable,
|
||||
$formStatsTable,
|
||||
'statisticsForms',
|
||||
"$subscribersTable.id = statisticsForms.subscriber_id AND statisticsForms.form_id IN (:$formIdsParam)"
|
||||
);
|
||||
$queryBuilder->andWhere("statisticsForms.subscriber_id IS NULL");
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($formIdsParam, $formIds, ArrayParameterType::INTEGER);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'forms' => [],
|
||||
];
|
||||
$formIds = $filterData->getArrayParam('form_ids');
|
||||
$forms = $this->formsRepository->findBy(['id' => $formIds]);
|
||||
foreach ($forms as $form) {
|
||||
$lookupData['forms'][$form->getId()] = $form->getName();
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
* The filters in this class are primarily intended for the premium plugin
|
||||
*/
|
||||
class SubscriberTag implements Filter {
|
||||
const TYPE = 'subscriberTag';
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$this->wp->applyFilters('mailpoet_dynamic_segments_filter_subscriber_tag_apply', $queryBuilder, $filter);
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$default = ['tags' => []];
|
||||
$filteredLookupData = $this->wp->applyFilters('mailpoet_dynamic_segments_filter_subscriber_tag_getLookupData', $default, $filterData);
|
||||
if (is_array($filteredLookupData)) {
|
||||
return $filteredLookupData;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class SubscriberTextField implements Filter {
|
||||
const FIRST_NAME = 'subscriberFirstName';
|
||||
const LAST_NAME = 'subscriberLastName';
|
||||
const EMAIL = 'subscriberEmail';
|
||||
|
||||
const TYPES = [self::FIRST_NAME, self::LAST_NAME, self::EMAIL];
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if (!is_string($operator)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$columnName = $this->getColumnNameForAction($action);
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('subscriberText');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_IS:
|
||||
$queryBuilder->andWhere("$columnName = :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_IS_NOT:
|
||||
$queryBuilder->andWhere("$columnName != :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($columnName, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($parameter, $value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getColumnNameForAction(string $field): string {
|
||||
switch ($field) {
|
||||
case self::FIRST_NAME:
|
||||
return 'first_name';
|
||||
case self::LAST_NAME:
|
||||
return 'last_name';
|
||||
case self::EMAIL:
|
||||
return 'email';
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Invalid action');
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\InvalidStateException;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class UserRole implements Filter {
|
||||
const TYPE = 'wordpressRole';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$filterData = $filter->getFilterData();
|
||||
$role = $filterData->getParam('wordpressRole');
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!$role) {
|
||||
throw new InvalidFilterException('Missing role', InvalidFilterException::MISSING_ROLE);
|
||||
}
|
||||
if (!is_array($role)) {
|
||||
// compatibility with the older segment before multiple roles were added
|
||||
$role = [$role];
|
||||
}
|
||||
if (!$operator) {
|
||||
$operator = DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
}
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$parameterSuffix = ((string)$filter->getId()) . Security::generateRandomString();
|
||||
$condition = $this->createCondition($role, $operator, $parameterSuffix);
|
||||
$qb = $queryBuilder->join($subscribersTable, $wpdb->users, 'wpusers', "$subscribersTable.wp_user_id = wpusers.id")
|
||||
->join('wpusers', $wpdb->usermeta, 'wpusermeta', 'wpusers.id = wpusermeta.user_id')
|
||||
->andWhere("wpusermeta.meta_key = '{$wpdb->prefix}capabilities' AND (" . $condition . ')');
|
||||
foreach ($role as $key => $userRole) {
|
||||
$qb->setParameter('role' . $key . $parameterSuffix, '%"' . $userRole . '"%');
|
||||
}
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $roles
|
||||
* @param string $operator
|
||||
* @param string $parameterSuffix
|
||||
* @return string
|
||||
*/
|
||||
private function createCondition(array $roles, string $operator, $parameterSuffix): string {
|
||||
$sqlParts = [];
|
||||
foreach ($roles as $key => $role) {
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$sqlParts[] = '(wpusermeta.meta_value NOT LIKE :role' . $key . $parameterSuffix . ')';
|
||||
} else {
|
||||
$sqlParts[] = '(wpusermeta.meta_value LIKE :role' . $key . $parameterSuffix . ')';
|
||||
}
|
||||
}
|
||||
if (($operator === DynamicSegmentFilterData::OPERATOR_NONE) || ($operator === DynamicSegmentFilterData::OPERATOR_ALL)) {
|
||||
return join(' AND ', $sqlParts);
|
||||
}
|
||||
return join(' OR ', $sqlParts);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
global $wp_roles;
|
||||
$lookupData = [
|
||||
'roles' => [],
|
||||
];
|
||||
$roles = $filterData->getParam('wordpressRole');
|
||||
if (is_string($roles)) {
|
||||
$roles = [$roles];
|
||||
}
|
||||
if (!is_array($roles)) {
|
||||
throw new InvalidStateException();
|
||||
}
|
||||
foreach ($roles as $roleSlug) {
|
||||
$roleData = $wp_roles->roles[$roleSlug] ?? null;
|
||||
if (is_array($roleData)) {
|
||||
$lookupData['roles'][$roleSlug] = $roleData['name'];
|
||||
}
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceAverageSpent implements Filter {
|
||||
const ACTION = 'averageSpent';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('average_spent_type');
|
||||
$amount = $filterData->getParam('average_spent_amount');
|
||||
$timeframe = $filterData->getParam('timeframe');
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if ($timeframe !== DynamicSegmentFilterData::TIMEFRAME_ALL_TIME) {
|
||||
/** @var int $days */
|
||||
$days = $filterData->getParam('days');
|
||||
$days = intval($days);
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
$amountParam = $this->filterHelper->getUniqueParameterName('amount');
|
||||
if ($operator === '=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) = :$amountParam");
|
||||
} elseif ($operator === '!=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) != :$amountParam");
|
||||
} elseif ($operator === '>') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) > :$amountParam");
|
||||
} elseif ($operator === '<') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) < :$amountParam");
|
||||
} elseif ($operator === '<=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) <= :$amountParam");
|
||||
} elseif ($operator === '>=') {
|
||||
$queryBuilder->having("AVG($orderStatsAlias.total_sales) >= :$amountParam");
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($amountParam, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use WP_Term;
|
||||
|
||||
class WooCommerceCategory implements Filter {
|
||||
const ACTION_CATEGORY = 'purchasedCategory';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->wp = $wp;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
|
||||
$operator = $filterData->getOperator();
|
||||
$categoryIds = (array)$filterData->getParam('category_ids');
|
||||
$categoryIdswithChildrenIds = $this->getCategoriesWithChildren($categoryIds);
|
||||
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$parameterSuffix = (string)$parameterSuffix;
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$this->applyTermRelationshipsJoin($queryBuilder);
|
||||
$this->applyTermTaxonomyJoin($queryBuilder, $parameterSuffix);
|
||||
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$subQueryCount = 1;
|
||||
foreach ($categoryIds as $categoryId) {
|
||||
$uniqueParamaterSuffix = Security::generateRandomString();
|
||||
$categoryIdWithChildrenIds = $this->getCategoriesWithChildren([$categoryId]);
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$this->applyTermRelationshipsJoin($subQuery);
|
||||
$this->applyTermTaxonomyJoin($subQuery, $uniqueParamaterSuffix);
|
||||
$subQuery->setParameter("category_$uniqueParamaterSuffix", $categoryIdWithChildrenIds, ArrayParameterType::STRING);
|
||||
$alias = sprintf("subQuery%s", $subQueryCount);
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
sprintf("(%s)", $this->filterHelper->getInterpolatedSQL($subQuery)),
|
||||
$alias,
|
||||
"$subscribersTable.id = $alias.id"
|
||||
);
|
||||
$subQueryCount++;
|
||||
}
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
// subQuery with subscriber ids that bought products
|
||||
$subQuery = $this->createQueryBuilder($subscribersTable);
|
||||
$subQuery->select("DISTINCT $subscribersTable.id");
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$subQuery = $this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$subQuery = $this->applyTermRelationshipsJoin($subQuery);
|
||||
$subQuery = $this->applyTermTaxonomyJoin($subQuery, $parameterSuffix);
|
||||
// apply subQuery for negation
|
||||
$queryBuilder->where("$subscribersTable.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->setParameter("category_$parameterSuffix", $categoryIdswithChildrenIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$wpdb->prefix . 'wc_order_product_lookup',
|
||||
'product',
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyTermRelationshipsJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->join(
|
||||
'product',
|
||||
$wpdb->term_relationships, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'term_relationships',
|
||||
'product.product_id = term_relationships.object_id'
|
||||
);
|
||||
}
|
||||
|
||||
private function applyTermTaxonomyJoin(QueryBuilder $queryBuilder, string $parameterSuffix): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'term_relationships',
|
||||
$wpdb->term_taxonomy, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
'term_taxonomy',
|
||||
"term_taxonomy.term_taxonomy_id=term_relationships.term_taxonomy_id
|
||||
AND
|
||||
term_taxonomy.term_id IN (:category_$parameterSuffix)"
|
||||
);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(string $table): QueryBuilder {
|
||||
return $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($table);
|
||||
}
|
||||
|
||||
private function getCategoriesWithChildren(array $categoriesId): array {
|
||||
$allIds = [];
|
||||
|
||||
foreach ($categoriesId as $categoryId) {
|
||||
$allIds = array_merge($allIds, $this->getAllCategoryIds($categoryId));
|
||||
}
|
||||
|
||||
return array_unique($allIds);
|
||||
}
|
||||
|
||||
private function getAllCategoryIds(int $categoryId): array {
|
||||
$subcategories = $this->wp->getTerms(['taxonomy' => 'product_cat', 'child_of' => $categoryId, 'hide_empty' => false]);
|
||||
if (!is_array($subcategories) || empty($subcategories)) {
|
||||
return [$categoryId];
|
||||
}
|
||||
$ids = array_map(function($category) {
|
||||
return $category->term_id; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}, $subcategories);
|
||||
$ids[] = $categoryId;
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'categories' => [],
|
||||
];
|
||||
$categoryIds = $filterData->getArrayParam('category_ids');
|
||||
$terms = $this->wp->getTerms(['taxonomy' => 'product_cat', 'include' => $categoryIds, 'hide_empty' => false]);
|
||||
/** @var WP_Term[] $terms */
|
||||
foreach ($terms as $term) {
|
||||
$lookupData['categories'][$term->term_id] = $term->name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceCountry implements Filter {
|
||||
const ACTION_CUSTOMER_COUNTRY = 'customerInCountry';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$filterData = $filter->getFilterData();
|
||||
$countryCode = $filterData->getParam('country_code');
|
||||
if (!is_array($countryCode)) {
|
||||
$countryCode = [(string)$countryCode];
|
||||
}
|
||||
$operator = $filterData->getParam('operator');
|
||||
if (!$operator) {
|
||||
$operator = DynamicSegmentFilterData::OPERATOR_ANY;
|
||||
}
|
||||
|
||||
$countryFilterParam = ((string)$filter->getId()) . Security::generateRandomString();
|
||||
$condition = $this->createCondition($countryCode, $operator, $countryFilterParam);
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'email'
|
||||
);
|
||||
$qb = $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'customer',
|
||||
"$subscribersTable.email = customer.email $collation"
|
||||
)->where($condition);
|
||||
|
||||
foreach ($countryCode as $key => $userCountryCode) {
|
||||
$qb->setParameter('countryCode' . $key . $countryFilterParam, '%' . $userCountryCode . '%');
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function createCondition(array $countryCodes, string $operator, string $countryFilterParam): string {
|
||||
$sqlParts = [];
|
||||
foreach ($countryCodes as $key => $userCountryCode) {
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$sqlParts[] = '(customer.country NOT LIKE :countryCode' . $key . $countryFilterParam . ')';
|
||||
} else {
|
||||
$sqlParts[] = '(customer.country LIKE :countryCode' . $key . $countryFilterParam . ')';
|
||||
}
|
||||
}
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
return join(' AND ', $sqlParts);
|
||||
}
|
||||
return join(' OR ', $sqlParts);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceCustomerTextField implements Filter {
|
||||
const CITY = 'customerInCity';
|
||||
const POSTAL_CODE = 'customerInPostalCode';
|
||||
|
||||
const ACTIONS = [self::CITY, self::POSTAL_CODE];
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$action = $filterData->getParam('action');
|
||||
$value = $filterData->getParam('value');
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
if (!is_string($action)) {
|
||||
throw new InvalidFilterException('Missing action', InvalidFilterException::MISSING_ACTION);
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidFilterException('Missing value', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
if (!is_string($operator)) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$customerLookupAlias = $this->wooFilterHelper->applyCustomerLookupJoin($queryBuilder);
|
||||
$column = sprintf("%s.%s", $customerLookupAlias, $this->getColumnNameForAction($action));
|
||||
$parameter = $this->filterHelper->getUniqueParameterName('customerTextField');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_IS:
|
||||
$queryBuilder->andWhere("$column = :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_IS_NOT:
|
||||
$queryBuilder->andWhere("$column != :$parameter");
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_CONTAINS:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_STARTS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = Helpers::escapeSearch($value) . '%';
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->like($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NOT_ENDS_WITH:
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notLike($column, ":$parameter"));
|
||||
$value = '%' . Helpers::escapeSearch($value);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($parameter, $value);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function getColumnNameForAction(string $field): string {
|
||||
switch ($field) {
|
||||
case self::CITY:
|
||||
return 'city';
|
||||
case self::POSTAL_CODE:
|
||||
return 'postcode';
|
||||
}
|
||||
|
||||
throw new InvalidFilterException('Invalid action');
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceFirstOrder implements Filter {
|
||||
const ACTION = 'firstOrder';
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$dateValue = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $dateValue);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if (in_array($operator, [DateFilterHelper::NOT_ON, DateFilterHelper::NOT_IN_THE_LAST])) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $subQuery);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("{$subscribersTable}.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
} else {
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $queryBuilder);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyConditionsToQueryBuilder(string $operator, string $date, QueryBuilder $queryBuilder): QueryBuilder {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
$queryBuilder->groupBy("$subscribersTable.id");
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) < :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) > :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) >= :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) = :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andHaving("MIN($orderStatsAlias.date_created) <= :$dateParam");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$queryBuilder->setParameter($dateParam, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceMembership implements Filter {
|
||||
const ACTION_MEMBER_OF = 'isMemberOf';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
/** @var array */
|
||||
$planIds = $filterData->getParam('plan_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
// ALL OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyPostJoin($queryBuilder);
|
||||
$this->applyParentPostJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")")
|
||||
->groupBy("$subscribersTable.id")
|
||||
->having("COUNT($subscribersTable.id) = :count$parameterSuffix")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING)
|
||||
->setParameter('count' . $parameterSuffix, count($planIds));
|
||||
}
|
||||
|
||||
// NONE OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQueryBuilder = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscribersTable)
|
||||
->select("DISTINCT $subscribersTable.id");
|
||||
$this->applyPostJoin($subQueryBuilder);
|
||||
$this->applyParentPostJoin($subQueryBuilder);
|
||||
$subQueryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")");
|
||||
return $queryBuilder->where("{$subscribersTable}.id NOT IN ({$subQueryBuilder->getSQL()})")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
// ANY
|
||||
$this->applyPostJoin($queryBuilder);
|
||||
$this->applyParentPostJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("posts.post_parent IN (:plans" . $parameterSuffix . ")")
|
||||
->setParameter('plans' . $parameterSuffix, $planIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->posts,
|
||||
'posts',
|
||||
"posts.post_type = 'wc_user_membership' AND posts.post_status IN ('wcm-active', 'wcm-complimentary', 'wcm-free_trial', 'wcm-pending') AND posts.post_author=$subscribersTable.wp_user_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyParentPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'posts',
|
||||
$wpdb->posts,
|
||||
'parentposts',
|
||||
"posts.post_parent = parentposts.id AND parentposts.post_type = 'wc_membership_plan' AND parentposts.post_status = 'publish'"
|
||||
);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceNumberOfOrders implements Filter {
|
||||
const ACTION_NUMBER_OF_ORDERS = 'numberOfOrders';
|
||||
const ACTION_NUMBER_OF_ORDERS_WITH_COUPON = 'numberOfOrdersWithCoupon';
|
||||
|
||||
const ACTIONS = [
|
||||
self::ACTION_NUMBER_OF_ORDERS,
|
||||
self::ACTION_NUMBER_OF_ORDERS_WITH_COUPON,
|
||||
];
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$filterData = $filter->getFilterData();
|
||||
/** @var string $type - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$type = $filterData->getParam('number_of_orders_type');
|
||||
$type = strval($type);
|
||||
/** @var string $count - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$count = $filterData->getParam('number_of_orders_count');
|
||||
$count = intval($count);
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_customer_lookup',
|
||||
'email'
|
||||
);
|
||||
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
|
||||
$joinCondition = $isAllTime
|
||||
? 'customer.customer_id = orderStats.customer_id AND orderStats.status IN (:allowedStatuses' . $parameterSuffix . ')'
|
||||
: 'customer.customer_id = orderStats.customer_id AND orderStats.date_created >= :date' . $parameterSuffix . ' AND orderStats.status IN (:allowedStatuses' . $parameterSuffix . ')';
|
||||
|
||||
$subQuery = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($wpdb->prefix . 'wc_customer_lookup', "customer")
|
||||
->select("customer.email $collation as email")
|
||||
->addSelect("orderStats.order_id as oder_stats_id")
|
||||
->leftJoin(
|
||||
'customer',
|
||||
$wpdb->prefix . 'wc_order_stats',
|
||||
'orderStats',
|
||||
$joinCondition
|
||||
);
|
||||
|
||||
$action = $filterData->getAction();
|
||||
|
||||
if ($action === self::ACTION_NUMBER_OF_ORDERS_WITH_COUPON) {
|
||||
$subQuery->innerJoin('orderStats', $wpdb->prefix . 'wc_order_coupon_lookup', 'couponLookup', 'orderStats.order_id = couponLookup.order_id');
|
||||
}
|
||||
|
||||
$queryBuilder->add('join', [
|
||||
$subscribersTable => [
|
||||
/**
|
||||
* Based the combination of $type and $count we may need to include none-customer subscribers
|
||||
* in this case we'll need to leftJoin subscribers table to result of the sub-query defined above,
|
||||
* in all other cases innerJoin gets us the expected records.
|
||||
*/
|
||||
'joinType' => $this-> shouldIncludeNoneCustomerSubscribers($type, $count) ? 'left' : 'inner',
|
||||
'joinTable' => "({$subQuery->getSQL()})",
|
||||
'joinAlias' => 'selectedCustomers',
|
||||
'joinCondition' => "$subscribersTable.email = selectedCustomers.email $collation",
|
||||
],
|
||||
], \true)
|
||||
->setParameter('date' . $parameterSuffix, $date->toDateTimeString())
|
||||
->setParameter('allowedStatuses' . $parameterSuffix, $this->wooFilterHelper->defaultIncludedStatuses(), ArrayParameterType::STRING)
|
||||
->groupBy('inner_subscriber_id');
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) = :count' . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) != :count' . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) > :count' . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->having('COUNT(oder_stats_id) < :count' . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('count' . $parameterSuffix, $count, 'integer');
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function shouldIncludeNoneCustomerSubscribers(string $type, int $count): bool {
|
||||
if ($type === '=') {
|
||||
return $count === 0;
|
||||
} elseif ($type === '!=') {
|
||||
return true;
|
||||
} elseif ($type === '>') {
|
||||
return $count < 0;
|
||||
} elseif ($type === '<') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceNumberOfReviews implements Filter {
|
||||
const ACTION = 'numberOfReviews';
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
DBCollationChecker $collationChecker,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$commentsTable = $this->filterHelper->getPrefixedTable('comments');
|
||||
$commentMetaTable = $this->filterHelper->getPrefixedTable('commentmeta');
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
/** @var string $type - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$type = $filterData->getParam('count_type');
|
||||
$type = strval($type);
|
||||
/** @var string $rating - for PHPStan because strval() doesn't accept a value of mixed */
|
||||
$rating = $filterData->getParam('rating');
|
||||
$rating = strval($rating);
|
||||
/** @var int $days - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$days = $filterData->getParam('days');
|
||||
$days = intval($days);
|
||||
/** @var int $count - for PHPStan because intval() doesn't accept a value of mixed */
|
||||
$count = $filterData->getParam('count');
|
||||
$count = intval($count);
|
||||
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$commentsTable,
|
||||
'comment_author_email'
|
||||
);
|
||||
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$joinCondition = "$subscribersTable.email = comments.comment_author_email $collation
|
||||
AND comments.comment_type = 'review'";
|
||||
|
||||
if (!$isAllTime) {
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$joinCondition .= " AND comments.comment_date >= :$dateParam";
|
||||
$queryBuilder->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$commentMetaJoinCondition = "comments.comment_ID = commentmeta.comment_id AND commentmeta.meta_key = 'rating'";
|
||||
|
||||
if ($rating !== 'any') {
|
||||
$ratingParam = $this->filterHelper->getUniqueParameterName('rating');
|
||||
$commentMetaJoinCondition .= "AND commentmeta.meta_value = :$ratingParam";
|
||||
$queryBuilder->setParameter($ratingParam, $rating);
|
||||
}
|
||||
|
||||
$queryBuilder
|
||||
->leftJoin(
|
||||
$subscribersTable,
|
||||
$commentsTable,
|
||||
'comments',
|
||||
$joinCondition
|
||||
)->leftJoin(
|
||||
'comments',
|
||||
$commentMetaTable,
|
||||
'commentmeta',
|
||||
$commentMetaJoinCondition
|
||||
);
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
|
||||
switch ($type) {
|
||||
case '=':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) = :$countParam");
|
||||
break;
|
||||
case '!=':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) != :$countParam");
|
||||
break;
|
||||
case '>':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) > :$countParam");
|
||||
break;
|
||||
case '<':
|
||||
$queryBuilder->having("COUNT(commentmeta.meta_value) < :$countParam");
|
||||
break;
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($countParam, $count, 'integer');
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
if (!isset($data['rating'])) {
|
||||
throw new InvalidFilterException('Missing rating', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$validRatings = ['1', '2', '3', '4', '5', 'any'];
|
||||
if (!in_array($data['rating'], $validRatings, true)) {
|
||||
throw new InvalidFilterException('Invalid rating', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['count_type'])) {
|
||||
throw new InvalidFilterException('Missing count type', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$type = $data['count_type'];
|
||||
$validTypes = [
|
||||
'=',
|
||||
'!=',
|
||||
'>',
|
||||
'<',
|
||||
];
|
||||
if (!in_array($type, $validTypes, true)) {
|
||||
throw new InvalidFilterException('Invalid count type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
if (!isset($data['count'])) {
|
||||
throw new InvalidFilterException('Missing review count', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use WC_Product;
|
||||
|
||||
class WooCommerceProduct implements Filter {
|
||||
const ACTION_PRODUCT = 'purchasedProduct';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
FilterHelper $filterHelper,
|
||||
Helper $wooHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getOperator();
|
||||
$productIds = $filterData->getParam('product_ids');
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->andWhere("product.product_id IN (:products_{$parameterSuffix})");
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->andWhere("product.product_id IN (:products_{$parameterSuffix})")
|
||||
->groupBy("{$subscribersTable}.id, $orderStatsAlias.order_id")
|
||||
->having("COUNT($orderStatsAlias.order_id) = :count" . $parameterSuffix)
|
||||
->setParameter('count' . $parameterSuffix, count($productIds));
|
||||
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
// subQuery with subscriber ids that bought products
|
||||
$subQuery = $this->createQueryBuilder($subscribersTable);
|
||||
$subQuery->select("DISTINCT $subscribersTable.id");
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($subQuery);
|
||||
$subQuery = $this->applyProductJoin($subQuery, $orderStatsAlias);
|
||||
$subQuery->andWhere("product.product_id IN (:products_{$parameterSuffix})");
|
||||
// application subQuery for negation
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
return $queryBuilder
|
||||
->setParameter("products_{$parameterSuffix}", $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$wpdb->prefix . 'wc_order_product_lookup',
|
||||
'product',
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
}
|
||||
|
||||
private function createQueryBuilder(string $table): QueryBuilder {
|
||||
return $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($table);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['products' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$productIds = $filterData->getArrayParam('product_ids');
|
||||
foreach ($productIds as $productId) {
|
||||
$product = $this->wooHelper->wcGetProduct($productId);
|
||||
if ($product instanceof WC_Product) {
|
||||
$lookupData['products'][$productId] = $product->get_name();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommercePurchaseDate implements Filter {
|
||||
const ACTION = 'purchaseDate';
|
||||
|
||||
/** @var DateFilterHelper */
|
||||
private $dateFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
DateFilterHelper $dateFilterHelper,
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->dateFilterHelper = $dateFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$operator = $this->dateFilterHelper->getOperatorFromFilter($filter);
|
||||
$dateValue = $this->dateFilterHelper->getDateValueFromFilter($filter);
|
||||
$date = $this->dateFilterHelper->getDateStringForOperator($operator, $dateValue);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
if (in_array($operator, [DateFilterHelper::NOT_ON, DateFilterHelper::NOT_IN_THE_LAST])) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $subQuery);
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("{$subscribersTable}.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
} else {
|
||||
$this->applyConditionsToQueryBuilder($operator, $date, $queryBuilder);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyConditionsToQueryBuilder(string $operator, string $date, QueryBuilder $queryBuilder): QueryBuilder {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
|
||||
switch ($operator) {
|
||||
case DateFilterHelper::BEFORE:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) < :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::AFTER:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) > :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::IN_THE_LAST:
|
||||
case DateFilterHelper::NOT_IN_THE_LAST:
|
||||
case DateFilterHelper::ON_OR_AFTER:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) >= :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON:
|
||||
case DateFilterHelper::NOT_ON:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) = :$dateParam");
|
||||
break;
|
||||
case DateFilterHelper::ON_OR_BEFORE:
|
||||
$queryBuilder->andWhere("DATE($orderStatsAlias.date_created) <= :$dateParam");
|
||||
break;
|
||||
default:
|
||||
throw new InvalidFilterException('Incorrect value for operator', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter($dateParam, $date);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WP\Functions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommercePurchasedWithAttribute implements Filter {
|
||||
const ACTION = 'purchasedWithAttribute';
|
||||
|
||||
const TYPE_LOCAL = 'local';
|
||||
const TYPE_TAXONOMY = 'taxonomy';
|
||||
|
||||
private WooFilterHelper $wooFilterHelper;
|
||||
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
private Functions $wp;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Functions $wp
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$type = $filterData->getStringParam('attribute_type');
|
||||
|
||||
if ($type === self::TYPE_LOCAL) {
|
||||
$this->applyForLocalAttribute($queryBuilder, $filterData);
|
||||
} elseif ($type === self::TYPE_TAXONOMY) {
|
||||
$this->applyForTaxonomyAttribute($queryBuilder, $filterData);
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForTaxonomyAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$attributeTaxonomySlug = $filterData->getStringParam('attribute_taxonomy_slug');
|
||||
$attributeTermIds = $filterData->getArrayParam('attribute_term_ids');
|
||||
$termIdsParam = $this->filterHelper->getUniqueParameterName('attribute_term_ids');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$attributeAlias = $this->applyTaxonomyAttributeJoin($queryBuilder, $productAlias, $attributeTaxonomySlug);
|
||||
$queryBuilder->andWhere("$attributeAlias.term_id IN (:$termIdsParam)");
|
||||
$queryBuilder->setParameter($termIdsParam, $attributeTermIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias, string $alias = 'product'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_product_lookup'),
|
||||
$alias,
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
return $alias;
|
||||
}
|
||||
|
||||
private function applyTaxonomyAttributeJoin(QueryBuilder $queryBuilder, string $productAlias, $taxonomySlug, string $alias = 'attribute'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_product_attributes_lookup'),
|
||||
$alias,
|
||||
"product.product_id = attribute.product_id AND attribute.taxonomy = '$taxonomySlug'"
|
||||
);
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$type = $filterData->getStringParam('attribute_type');
|
||||
|
||||
if ($type !== self::TYPE_TAXONOMY) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$slug = $filterData->getStringParam('attribute_taxonomy_slug');
|
||||
|
||||
$lookupData = [
|
||||
'attribute' => $slug,
|
||||
];
|
||||
|
||||
$termIds = $filterData->getArrayParam('attribute_term_ids');
|
||||
$terms = $this->wp->getTerms([
|
||||
'taxonomy' => $slug,
|
||||
'include' => $termIds,
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
|
||||
$lookupData['terms'] = array_map(function($term) {
|
||||
return $term->name;
|
||||
}, $terms);
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$operator = $data['operator'] ?? null;
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
$this->validateAttributeData($data);
|
||||
}
|
||||
|
||||
public function validateAttributeData(array $data): void {
|
||||
$type = $data['attribute_type'];
|
||||
|
||||
if (!in_array($type, [self::TYPE_LOCAL, self::TYPE_TAXONOMY], true)) {
|
||||
throw new InvalidFilterException('Invalid attribute type', InvalidFilterException::INVALID_TYPE);
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_LOCAL) {
|
||||
$name = $data['attribute_local_name'] ?? null;
|
||||
if (!is_string($name) || strlen($name) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
$values = $data['attribute_local_values'] ?? [];
|
||||
if (!is_array($values) || count($values) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute values', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_TAXONOMY) {
|
||||
$attribute_taxonomy_slug = $data['attribute_taxonomy_slug'] ?? null;
|
||||
if (!is_string($attribute_taxonomy_slug) || strlen($attribute_taxonomy_slug) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
if (!isset($data['attribute_term_ids']) || !is_array($data['attribute_term_ids']) || count($data['attribute_term_ids']) === 0) {
|
||||
throw new InvalidFilterException('Missing attribute terms', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForTaxonomyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData) {
|
||||
$operator = $filterData->getOperator();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForTaxonomyAnyOperator($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT attribute.term_id) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('attribute_term_ids')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForTaxonomyAnyOperator($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForLocalAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$operator = $filterData->getOperator();
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForLocalAnyAttribute($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('count');
|
||||
$queryBuilder
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT postmeta.meta_value) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('attribute_local_values')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForLocalAnyAttribute($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->where("{$subscribersTable}.id NOT IN ({$this->filterHelper->getInterpolatedSQL($subQuery)})");
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForLocalAnyAttribute(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$attributeName = $filterData->getStringParam('attribute_local_name');
|
||||
$attributeValues = $filterData->getArrayParam('attribute_local_values');
|
||||
$valuesParam = $this->filterHelper->getUniqueParameterName('attribute_values');
|
||||
$keyParam = $this->filterHelper->getUniqueParameterName('attribute_name');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('postmeta'),
|
||||
'postmeta',
|
||||
"$productAlias.product_id = postmeta.post_id AND postmeta.meta_key = :$keyParam AND postmeta.meta_value IN (:$valuesParam)"
|
||||
);
|
||||
|
||||
$queryBuilder->setParameter($keyParam, sprintf("attribute_%s", $attributeName));
|
||||
$queryBuilder->setParameter($valuesParam, $attributeValues, ArrayParameterType::STRING);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceSingleOrderValue implements Filter {
|
||||
const ACTION_SINGLE_ORDER_VALUE = 'singleOrderValue';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$type = $filterData->getParam('single_order_value_type');
|
||||
$amount = $filterData->getParam('single_order_value_amount');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
$days = $filterData->getParam('days');
|
||||
if (!is_string($days)) {
|
||||
$days = '1'; // Default to last day
|
||||
}
|
||||
$date = Carbon::now()->subDays((int)$days);
|
||||
$dateParam = "date_$parameterSuffix";
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales = :amount" . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales != :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales > :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales >= :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales < :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<=') {
|
||||
$queryBuilder->andWhere("$orderStatsAlias.total_sales <= :amount" . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('amount' . $parameterSuffix, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
|
||||
class WooCommerceSubscription implements Filter {
|
||||
const ACTION_HAS_ACTIVE = 'hasActiveSubscription';
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $woocommerceHelper;
|
||||
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
DBCollationChecker $collationChecker,
|
||||
WooCommerceHelper $woocommerceHelper
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$productIds = $filterData->getParam('product_ids');
|
||||
$operator = $filterData->getParam('operator');
|
||||
$parameterSuffix = $filter->getId() ?: Security::generateRandomString();
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
|
||||
// ALL OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyPostmetaAndPostJoin($queryBuilder);
|
||||
$this->applyOrderItemsJoin($queryBuilder);
|
||||
$this->applyOrderItemmetaJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")")
|
||||
->groupBy("$subscribersTable.id")
|
||||
->having("COUNT($subscribersTable.id) = :count$parameterSuffix")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING)
|
||||
->setParameter('count' . $parameterSuffix, count($productIds));
|
||||
}
|
||||
|
||||
// NONE OF
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQueryBuilder = $this->entityManager->getConnection()
|
||||
->createQueryBuilder()
|
||||
->from($subscribersTable)
|
||||
->select("DISTINCT $subscribersTable.id");
|
||||
$this->applyPostmetaAndPostJoin($subQueryBuilder);
|
||||
$this->applyOrderItemsJoin($subQueryBuilder);
|
||||
$this->applyOrderItemmetaJoin($subQueryBuilder);
|
||||
$subQueryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")");
|
||||
return $queryBuilder->where("{$subscribersTable}.id NOT IN ({$subQueryBuilder->getSQL()})")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
// ANY
|
||||
$this->applyPostmetaAndPostJoin($queryBuilder);
|
||||
$this->applyOrderItemsJoin($queryBuilder);
|
||||
$this->applyOrderItemmetaJoin($queryBuilder);
|
||||
return $queryBuilder
|
||||
->andWhere("itemmeta.meta_value IN (:products" . $parameterSuffix . ")")
|
||||
->setParameter('products' . $parameterSuffix, $productIds, ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyPostmetaAndPostJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
$subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
'billing_email'
|
||||
);
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->prefix . 'wc_orders',
|
||||
'wc_orders',
|
||||
"{$subscribersTable}.email = wc_orders.billing_email $collation AND wc_orders.status IN('wc-active', 'wc-pending-cancel')"
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$wpdb->postmeta,
|
||||
'postmeta',
|
||||
"postmeta.meta_key = '_customer_user' AND $subscribersTable.wp_user_id=postmeta.meta_value"
|
||||
)->innerJoin(
|
||||
'postmeta',
|
||||
$wpdb->posts,
|
||||
'posts',
|
||||
"postmeta.post_id = posts.id AND posts.post_type = 'shop_subscription' AND posts.post_status IN('wc-active', 'wc-pending-cancel')"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyOrderItemsJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
if ($this->woocommerceHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
return $queryBuilder->innerJoin(
|
||||
'wc_orders',
|
||||
$wpdb->prefix . 'woocommerce_order_items',
|
||||
'items',
|
||||
"wc_orders.id = items.order_id AND order_item_type = 'line_item'"
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->innerJoin(
|
||||
'postmeta',
|
||||
$wpdb->prefix . 'woocommerce_order_items',
|
||||
'items',
|
||||
"postmeta.post_id = items.order_id AND order_item_type = 'line_item'"
|
||||
);
|
||||
}
|
||||
|
||||
private function applyOrderItemmetaJoin(QueryBuilder $queryBuilder): QueryBuilder {
|
||||
global $wpdb;
|
||||
return $queryBuilder->innerJoin(
|
||||
'items',
|
||||
$wpdb->prefix . 'woocommerce_order_itemmeta',
|
||||
'itemmeta',
|
||||
"itemmeta.order_item_id=items.order_item_id AND itemmeta.meta_key='_product_id'"
|
||||
);
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use WP_Term;
|
||||
|
||||
class WooCommerceTag implements Filter {
|
||||
const ACTION = 'purchasedTag';
|
||||
|
||||
private WPFunctions $wp;
|
||||
private WooFilterHelper $wooFilterHelper;
|
||||
private FilterHelper $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$operator = $filterData->getOperator();
|
||||
|
||||
if ($operator === DynamicSegmentFilterData::OPERATOR_ANY) {
|
||||
$this->applyForAnyOperator($queryBuilder, $filterData);
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_ALL) {
|
||||
$this->applyForAnyOperator($queryBuilder, $filterData);
|
||||
$countParam = $this->filterHelper->getUniqueParameterName('tagCount');
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT term_taxonomy.term_id) = :$countParam")
|
||||
->setParameter($countParam, count($filterData->getArrayParam('tag_ids')));
|
||||
} elseif ($operator === DynamicSegmentFilterData::OPERATOR_NONE) {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $filterData);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function applyForAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterData $filterData): void {
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
$tagIdsParam = $this->filterHelper->getUniqueParameterName('tagIds');
|
||||
$productAlias = $this->applyProductJoin($queryBuilder, $orderStatsAlias);
|
||||
$queryBuilder->join(
|
||||
$productAlias,
|
||||
$this->filterHelper->getPrefixedTable('term_relationships'),
|
||||
'term_relationships',
|
||||
'product.product_id = term_relationships.object_id'
|
||||
);
|
||||
$queryBuilder->innerJoin(
|
||||
'term_relationships',
|
||||
$this->filterHelper->getPrefixedTable('term_taxonomy'),
|
||||
'term_taxonomy',
|
||||
"term_taxonomy.term_taxonomy_id = term_relationships.term_taxonomy_id
|
||||
AND
|
||||
term_taxonomy.term_id IN (:$tagIdsParam)"
|
||||
);
|
||||
$queryBuilder->setParameter($tagIdsParam, $filterData->getArrayParam('tag_ids'), ArrayParameterType::STRING);
|
||||
}
|
||||
|
||||
private function applyProductJoin(QueryBuilder $queryBuilder, string $orderStatsAlias, string $productAlias = 'product'): string {
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_product_lookup'),
|
||||
$productAlias,
|
||||
"$orderStatsAlias.order_id = product.order_id"
|
||||
);
|
||||
return $productAlias;
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$operator = $data['operator'] ?? null;
|
||||
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($data['tag_ids'] ?? null) || count($data['tag_ids']) === 0) {
|
||||
throw new InvalidFilterException('Missing tag ids');
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'tags' => [],
|
||||
];
|
||||
$tagIds = $filterData->getArrayParam('tag_ids');
|
||||
$terms = $this->wp->getTerms(['taxonomy' => 'product_tag', 'include' => $tagIds, 'hide_empty' => false]);
|
||||
/** @var WP_Term[] $terms */
|
||||
foreach ($terms as $term) {
|
||||
$lookupData['tags'][$term->term_id] = $term->name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Util\Security;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceTotalSpent implements Filter {
|
||||
const ACTION_TOTAL_SPENT = 'totalSpent';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$type = $filterData->getParam('total_spent_type');
|
||||
$amount = $filterData->getParam('total_spent_amount');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$parameterSuffix = $filter->getId() ?? Security::generateRandomString();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = "date_$parameterSuffix";
|
||||
$queryBuilder->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy('inner_subscriber_id');
|
||||
|
||||
if ($type === '=') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) = :amount" . $parameterSuffix);
|
||||
} elseif ($type === '!=') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) != :amount" . $parameterSuffix);
|
||||
} elseif ($type === '>') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) > :amount" . $parameterSuffix);
|
||||
} elseif ($type === '<') {
|
||||
$queryBuilder->having("SUM($orderStatsAlias.total_sales) < :amount" . $parameterSuffix);
|
||||
}
|
||||
|
||||
$queryBuilder->setParameter('amount' . $parameterSuffix, $amount);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceUsedCouponCode implements Filter {
|
||||
const ACTION = 'usedCouponCode';
|
||||
|
||||
const COUPON_CODE_IDS_KEY = 'coupon_code_ids';
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
public function __construct(
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$this->validateFilterData((array)$filterData->getData());
|
||||
|
||||
$operator = $filterData->getParam('operator');
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $filter);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $filter);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $filter);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): void {
|
||||
$filterData = $filter->getFilterData();
|
||||
$couponIds = (array)$filterData->getParam(self::COUPON_CODE_IDS_KEY);
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder);
|
||||
|
||||
if (!$isAllTime) {
|
||||
/** @var int $days */
|
||||
$days = $filterData->getParam('days');
|
||||
$date = Carbon::now()->subDays($days);
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$orderStatsAlias,
|
||||
$this->filterHelper->getPrefixedTable('wc_order_coupon_lookup'),
|
||||
'couponLookup',
|
||||
"$orderStatsAlias.order_id = couponLookup.order_id"
|
||||
);
|
||||
$couponCodeIdsParam = $this->filterHelper->getUniqueParameterName('couponCodeIds');
|
||||
$queryBuilder
|
||||
->andWhere("couponLookup.coupon_id IN (:$couponCodeIdsParam)")
|
||||
->setParameter($couponCodeIdsParam, $couponIds, ArrayParameterType::INTEGER);
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): void {
|
||||
$this->applyForAnyOperator($queryBuilder, $filter);
|
||||
|
||||
$filterData = $filter->getFilterData();
|
||||
$couponIds = (array)$filterData->getParam(self::COUPON_CODE_IDS_KEY);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT couponLookup.coupon_id) = " . count(array_unique($couponIds)));
|
||||
}
|
||||
|
||||
public function validateFilterData(array $data): void {
|
||||
$this->filterHelper->validateDaysPeriodData($data);
|
||||
$couponCodeIds = $data[self::COUPON_CODE_IDS_KEY] ?? [];
|
||||
if (count($couponCodeIds) === 0) {
|
||||
throw new InvalidFilterException('Missing coupon code IDs', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$operator = $data['operator'] ?? null;
|
||||
|
||||
if (
|
||||
!in_array($operator, [
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
])
|
||||
) {
|
||||
throw new InvalidFilterException('Missing operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['coupons' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$couponIds = $filterData->getArrayParam(self::COUPON_CODE_IDS_KEY);
|
||||
foreach ($couponIds as $couponId) {
|
||||
$couponCode = $this->wooHelper->wcGetCouponCodeById((int)$couponId);
|
||||
if (!empty($couponCode)) {
|
||||
$lookupData['coupons'][$couponId] = $couponCode;
|
||||
}
|
||||
}
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
use WC_Payment_Gateway;
|
||||
|
||||
class WooCommerceUsedPaymentMethod implements Filter {
|
||||
const ACTION = 'usedPaymentMethod';
|
||||
|
||||
const VALID_OPERATORS = [
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
];
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
$paymentMethods = $filterData->getParam('payment_methods');
|
||||
$days = $filterData->getParam('days');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($paymentMethods) || count($paymentMethods) < 1) {
|
||||
throw new InvalidFilterException('Missing payment methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$data = $filterData->getData();
|
||||
$this->filterHelper->validateDaysPeriodData((array)$data);
|
||||
|
||||
$includedStatuses = array_keys($this->wooHelper->getOrderStatuses());
|
||||
$failedKey = array_search('wc-failed', $includedStatuses, true);
|
||||
if ($failedKey !== false) {
|
||||
unset($includedStatuses[$failedKey]);
|
||||
}
|
||||
$date = is_int($days) ? Carbon::now()->subDays($days) : Carbon::now();
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime): void {
|
||||
if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
} else {
|
||||
$this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime): void {
|
||||
if ($this->wooHelper->isWooCommerceCustomOrdersTableEnabled()) {
|
||||
$ordersAlias = $this->applyCustomOrderTableJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT $ordersAlias.payment_method) = " . count($paymentMethods));
|
||||
} else {
|
||||
$postmetaAlias = $this->applyPostmetaOrderJoin($queryBuilder, $includedStatuses, $paymentMethods, $date, $isAllTime);
|
||||
$queryBuilder->groupBy('inner_subscriber_id')->having("COUNT(DISTINCT $postmetaAlias.meta_value) = " . count($paymentMethods));
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPostmetaOrderJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime, string $postmetaAlias = 'postmeta'): string {
|
||||
$paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
$paymentMethodMetaKeyParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
|
||||
$postMetaTable = $this->filterHelper->getPrefixedTable('postmeta');
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $postMetaTable, $postmetaAlias, "$orderStatsAlias.order_id = $postmetaAlias.post_id")
|
||||
->andWhere("postmeta.meta_key = :$paymentMethodMetaKeyParam")
|
||||
->andWhere("postmeta.meta_value IN (:$paymentMethodParam)")
|
||||
->setParameter($paymentMethodMetaKeyParam, '_payment_method')
|
||||
->setParameter($paymentMethodParam, $paymentMethods, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
return $postmetaAlias;
|
||||
}
|
||||
|
||||
private function applyCustomOrderTableJoin(QueryBuilder $queryBuilder, array $includedStatuses, array $paymentMethods, Carbon $date, bool $isAllTime, string $ordersAlias = 'orders'): string {
|
||||
$paymentMethodParam = $this->filterHelper->getUniqueParameterName('paymentMethod');
|
||||
$ordersTable = $this->wooHelper->getOrdersTableName();
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $ordersTable, 'orders', "$orderStatsAlias.order_id = orders.id")
|
||||
->andWhere("$ordersAlias.payment_method IN (:$paymentMethodParam)")
|
||||
->setParameter($paymentMethodParam, $paymentMethods, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
return $ordersAlias;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = [
|
||||
'paymentMethods' => [],
|
||||
];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$paymentMethods = $filterData->getArrayParam('payment_methods');
|
||||
$allGateways = $this->wooHelper->getPaymentGateways()->payment_gateways();
|
||||
|
||||
foreach ($paymentMethods as $paymentMethod) {
|
||||
if (isset($allGateways[$paymentMethod]) && $allGateways[$paymentMethod] instanceof WC_Payment_Gateway) {
|
||||
$lookupData['paymentMethods'][$paymentMethod] = $allGateways[$paymentMethod]->get_method_title();
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\DynamicSegmentFilterEntity;
|
||||
use MailPoet\Segments\DynamicSegments\Exceptions\InvalidFilterException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooCommerceUsedShippingMethod implements Filter {
|
||||
const ACTION = 'usedShippingMethod';
|
||||
|
||||
const VALID_OPERATORS = [
|
||||
DynamicSegmentFilterData::OPERATOR_NONE,
|
||||
DynamicSegmentFilterData::OPERATOR_ANY,
|
||||
DynamicSegmentFilterData::OPERATOR_ALL,
|
||||
];
|
||||
|
||||
/** @var WooFilterHelper */
|
||||
private $wooFilterHelper;
|
||||
|
||||
/** @var Helper */
|
||||
private $wooHelper;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
FilterHelper $filterHelper,
|
||||
WooFilterHelper $wooFilterHelper,
|
||||
Helper $wooHelper
|
||||
) {
|
||||
$this->wooFilterHelper = $wooFilterHelper;
|
||||
$this->wooHelper = $wooHelper;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder, DynamicSegmentFilterEntity $filter): QueryBuilder {
|
||||
$filterData = $filter->getFilterData();
|
||||
$operator = $filterData->getParam('operator');
|
||||
$shippingMethodInstanceIds = $filterData->getParam('shipping_methods');
|
||||
$isAllTime = $filterData->getParam('timeframe') === DynamicSegmentFilterData::TIMEFRAME_ALL_TIME;
|
||||
|
||||
$days = $filterData->getParam('days');
|
||||
|
||||
if (!is_string($operator) || !in_array($operator, self::VALID_OPERATORS, true)) {
|
||||
throw new InvalidFilterException('Invalid operator', InvalidFilterException::MISSING_OPERATOR);
|
||||
}
|
||||
|
||||
if (!is_array($shippingMethodInstanceIds) || empty($shippingMethodInstanceIds)) {
|
||||
throw new InvalidFilterException('Missing shipping methods', InvalidFilterException::MISSING_VALUE);
|
||||
}
|
||||
|
||||
$data = $filterData->getData();
|
||||
$this->filterHelper->validateDaysPeriodData((array)$data);
|
||||
|
||||
$includedStatuses = array_keys($this->wooHelper->getOrderStatuses());
|
||||
$failedKey = array_search('wc-failed', $includedStatuses, true);
|
||||
if ($failedKey !== false) {
|
||||
unset($includedStatuses[$failedKey]);
|
||||
}
|
||||
$date = is_int($days) ? Carbon::now()->subDays($days) : Carbon::now();
|
||||
|
||||
switch ($operator) {
|
||||
case DynamicSegmentFilterData::OPERATOR_ANY:
|
||||
$this->applyForAnyOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_ALL:
|
||||
$this->applyForAllOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
case DynamicSegmentFilterData::OPERATOR_NONE:
|
||||
$this->applyForNoneOperator($queryBuilder, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
break;
|
||||
}
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
public function getLookupData(DynamicSegmentFilterData $filterData): array {
|
||||
$lookupData = ['shippingMethods' => []];
|
||||
if (!$this->wooHelper->isWooCommerceActive()) {
|
||||
return $lookupData;
|
||||
}
|
||||
$allMethods = $this->wooHelper->getShippingMethodInstancesData();
|
||||
$configuredShippingMethodInstanceIds = $filterData->getArrayParam('shipping_methods');
|
||||
|
||||
foreach ($configuredShippingMethodInstanceIds as $instanceId) {
|
||||
if (isset($allMethods[$instanceId])) {
|
||||
$data = $allMethods[$instanceId];
|
||||
$lookupData['shippingMethods'][$instanceId] = $data['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $lookupData;
|
||||
}
|
||||
|
||||
private function applyForAnyOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
|
||||
|
||||
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
|
||||
$orderItemsTableAlias = 'orderItems';
|
||||
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
|
||||
$orderItemMetaTableAlias = 'orderItemMeta';
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
|
||||
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
|
||||
->andWhere("$orderItemsTableAlias.order_item_type = 'shipping'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
|
||||
->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING);
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForAllOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$orderItemTypeParam = $this->filterHelper->getUniqueParameterName('orderItemType');
|
||||
$instanceIdsParam = $this->filterHelper->getUniqueParameterName('instanceIds');
|
||||
|
||||
$orderItemsTable = $this->filterHelper->getPrefixedTable('woocommerce_order_items');
|
||||
$orderItemsTableAlias = 'orderItems';
|
||||
$orderItemMetaTable = $this->filterHelper->getPrefixedTable('woocommerce_order_itemmeta');
|
||||
$orderItemMetaTableAlias = 'orderItemMeta';
|
||||
$orderStatsAlias = $this->wooFilterHelper->applyOrderStatusFilter($queryBuilder, $includedStatuses);
|
||||
|
||||
$queryBuilder
|
||||
->innerJoin($orderStatsAlias, $orderItemsTable, $orderItemsTableAlias, "$orderStatsAlias.order_id = $orderItemsTableAlias.order_id")
|
||||
->innerJoin($orderItemsTableAlias, $orderItemMetaTable, $orderItemMetaTableAlias, "$orderItemsTableAlias.order_item_id = $orderItemMetaTableAlias.order_item_id")
|
||||
->andWhere("$orderItemsTableAlias.order_item_type = :$orderItemTypeParam")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_key = 'instance_id'")
|
||||
->andWhere("$orderItemMetaTableAlias.meta_value IN (:$instanceIdsParam)")
|
||||
->setParameter($orderItemTypeParam, 'shipping')
|
||||
->setParameter($instanceIdsParam, $shippingMethodInstanceIds, ArrayParameterType::STRING)
|
||||
->groupBy('inner_subscriber_id')
|
||||
->having("COUNT(DISTINCT($orderItemMetaTableAlias.meta_value)) = " . count($shippingMethodInstanceIds));
|
||||
|
||||
if (!$isAllTime) {
|
||||
$dateParam = $this->filterHelper->getUniqueParameterName('date');
|
||||
$queryBuilder
|
||||
->andWhere("$orderStatsAlias.date_created >= :$dateParam")
|
||||
->setParameter($dateParam, $date->toDateTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
private function applyForNoneOperator(QueryBuilder $queryBuilder, array $includedStatuses, array $shippingMethodInstanceIds, Carbon $date, bool $isAllTime): void {
|
||||
$subQuery = $this->filterHelper->getNewSubscribersQueryBuilder();
|
||||
$this->applyForAnyOperator($subQuery, $includedStatuses, $shippingMethodInstanceIds, $date, $isAllTime);
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->notIn("$subscribersTable.id", $this->filterHelper->getInterpolatedSQL($subQuery)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments\Filters;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Util\DBCollationChecker;
|
||||
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
class WooFilterHelper {
|
||||
/** @var DBCollationChecker */
|
||||
private $collationChecker;
|
||||
|
||||
/** @var FilterHelper */
|
||||
private $filterHelper;
|
||||
|
||||
public function __construct(
|
||||
DBCollationChecker $collationChecker,
|
||||
FilterHelper $filterHelper
|
||||
) {
|
||||
$this->collationChecker = $collationChecker;
|
||||
$this->filterHelper = $filterHelper;
|
||||
}
|
||||
|
||||
public function defaultIncludedStatuses(): array {
|
||||
return ['wc-processing', 'wc-completed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param string $customerAlias
|
||||
* @return string - The alias of the joined customer lookup table
|
||||
*/
|
||||
public function applyCustomerLookupJoin(QueryBuilder $queryBuilder, string $customerAlias = 'customer'): string {
|
||||
$subscribersTable = $this->filterHelper->getSubscribersTable();
|
||||
|
||||
$collation = $this->collationChecker->getCollateIfNeeded(
|
||||
$subscribersTable,
|
||||
'email',
|
||||
$this->customerLookupTable(),
|
||||
'email'
|
||||
);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$subscribersTable,
|
||||
$this->customerLookupTable(),
|
||||
$customerAlias,
|
||||
"$subscribersTable.email = $customerAlias.email $collation"
|
||||
);
|
||||
|
||||
return $customerAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param string $orderStatsAlias
|
||||
* @return string - The alias of the joined order stats table
|
||||
*/
|
||||
public function applyCustomerOrderJoin(QueryBuilder $queryBuilder, string $orderStatsAlias = 'orderStats'): string {
|
||||
$customerAlias = $this->applyCustomerLookupJoin($queryBuilder);
|
||||
|
||||
$queryBuilder->innerJoin(
|
||||
$customerAlias,
|
||||
$this->orderStatsTable(),
|
||||
$orderStatsAlias,
|
||||
"$customerAlias.customer_id = $orderStatsAlias.customer_id"
|
||||
);
|
||||
|
||||
return $orderStatsAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param array|null $allowedStatuses
|
||||
* @return string - The alias of the joined order stats table
|
||||
*/
|
||||
public function applyOrderStatusFilter(QueryBuilder $queryBuilder, array $allowedStatuses = null): string {
|
||||
if (is_null($allowedStatuses)) {
|
||||
$allowedStatuses = $this->defaultIncludedStatuses();
|
||||
}
|
||||
|
||||
$statusParam = $this->filterHelper->getUniqueParameterName('status');
|
||||
$orderStatsAlias = $this->applyCustomerOrderJoin($queryBuilder);
|
||||
$queryBuilder->andWhere("$orderStatsAlias.status IN (:$statusParam)");
|
||||
$queryBuilder->setParameter($statusParam, $allowedStatuses, ArrayParameterType::STRING);
|
||||
return $orderStatsAlias;
|
||||
}
|
||||
|
||||
private function customerLookupTable(): string {
|
||||
return $this->filterHelper->getPrefixedTable('wc_customer_lookup');
|
||||
}
|
||||
|
||||
private function orderStatsTable(): string {
|
||||
return $this->filterHelper->getPrefixedTable('wc_order_stats');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Segments\DynamicSegments;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\ConflictException;
|
||||
use MailPoet\Entities\SegmentEntity;
|
||||
use MailPoet\NotFoundException;
|
||||
use MailPoet\Segments\SegmentsRepository;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\ORMException;
|
||||
|
||||
class SegmentSaveController {
|
||||
/** @var SegmentsRepository */
|
||||
private $segmentsRepository;
|
||||
|
||||
/** @var FilterDataMapper */
|
||||
private $filterDataMapper;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $entityManager;
|
||||
|
||||
public function __construct(
|
||||
SegmentsRepository $segmentsRepository,
|
||||
FilterDataMapper $filterDataMapper,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
$this->filterDataMapper = $filterDataMapper;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConflictException
|
||||
* @throws NotFoundException
|
||||
* @throws Exceptions\InvalidFilterException
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function save(array $data = []): SegmentEntity {
|
||||
$id = isset($data['id']) ? (int)$data['id'] : null;
|
||||
$name = $data['name'] ?? '';
|
||||
|
||||
if (!$this->segmentsRepository->isNameUnique($name, null) && isset($data['force_creation']) && $data['force_creation'] === 'true') {
|
||||
$name = $name . ' (' . wp_generate_password(5, false) . ')';
|
||||
}
|
||||
|
||||
$description = $data['description'] ?? '';
|
||||
$filtersData = $this->filterDataMapper->map($data);
|
||||
|
||||
return $this->segmentsRepository->createOrUpdate($name, $description, SegmentEntity::TYPE_DYNAMIC, $filtersData, $id);
|
||||
}
|
||||
|
||||
public function duplicate(SegmentEntity $segmentEntity): SegmentEntity {
|
||||
$duplicate = clone $segmentEntity;
|
||||
// translators: %s is the name of the segment
|
||||
$duplicate->setName(sprintf(__('Copy of %s', 'mailpoet'), $segmentEntity->getName()));
|
||||
$this->segmentsRepository->verifyNameIsUnique($duplicate->getName(), $duplicate->getId());
|
||||
$this->entityManager->wrapInTransaction(function(EntityManager $entityManager) use ($duplicate, $segmentEntity) {
|
||||
foreach ($segmentEntity->getDynamicFilters() as $dynamicFilter) {
|
||||
$duplicateFilter = clone $dynamicFilter;
|
||||
$duplicate->addDynamicFilter($duplicateFilter);
|
||||
$duplicateFilter->setSegment($duplicate);
|
||||
$entityManager->persist($duplicateFilter);
|
||||
}
|
||||
$entityManager->persist($duplicate);
|
||||
$entityManager->flush();
|
||||
});
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user