Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/bundles/AssetBundle/Entity/DownloadRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public function getDownloadCountsByPage($pageId, \DateTime $fromDate = null): ar
*
* @return array<mixed, array<string, mixed>>
*/
public function getDownloadCountsByEmail($emailId, \DateTime $fromDate = null): array
public function getDownloadCountsByEmail($emailId, \DateTime $fromDate = null, \DateTime $toDate = null): array
{
// link email to page hit tracking id to download tracking id
$q = $this->_em->getConnection()->createQueryBuilder();
Expand All @@ -200,6 +200,12 @@ public function getDownloadCountsByEmail($emailId, \DateTime $fromDate = null):
->setParameter('date', $dh->toUtcString());
}

if (null != $toDate) {
$dh = new DateTimeHelper($toDate);
$q->andWhere($q->expr()->lte('a.date_download', ':dateTo'))
->setParameter('dateTo', $dh->toUtcString());
}

$results = $q->executeQuery()->fetchAllAssociative();

$downloads = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function onDetermineDownloadRateWinner(DetermineWinnerEvent $event): void

$startDate = $parent->getVariantStartDate();
if (null != $startDate && !empty($ids)) {
$counts = ('page' == $type) ? $repo->getDownloadCountsByPage($ids, $startDate) : $repo->getDownloadCountsByEmail($ids, $startDate);
$counts = ('page' == $type) ? $repo->getDownloadCountsByPage($ids, $startDate) : $repo->getDownloadCountsByEmail($ids, $startDate, $parent->getVariantEndDate());

$translator = $this->translator;
if ($counts) {
Expand Down
31 changes: 31 additions & 0 deletions app/bundles/CoreBundle/Entity/VariantEntityInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@

interface VariantEntityInterface
{
/**
* Get id.
*
* @return int
*/
public function getId();

/**
* Check publish status with option to check against category, publish up and down dates.
*
* @param bool $checkPublishStatus
* @param bool $checkCategoryStatus
*
* @return bool
*/
public function isPublished($checkPublishStatus = true, $checkCategoryStatus = true);

/**
* Get translation parent.
*
Expand Down Expand Up @@ -75,4 +92,18 @@ public function getVariants();
* @return bool
*/
public function isVariant($isChild = false);

/**
* Sets settings array for the variant.
*
* @param array<int|string> $variantSettings
*/
public function setVariantSettings($variantSettings): self;

/**
* @param \DateTimeInterface|null $variantStartDate
*/
public function setVariantStartDate($variantStartDate): self;

public function isParent(): bool;
}
118 changes: 108 additions & 10 deletions app/bundles/CoreBundle/Entity/VariantEntityTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Entity;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Model\AbTest\AbTestSettingsService;
use Mautic\DynamicContentBundle\Entity\DynamicContent;
use Mautic\EmailBundle\Entity\Email;
use Mautic\PageBundle\Entity\Page;

trait VariantEntityTrait
{
Expand All @@ -15,14 +19,24 @@ trait VariantEntityTrait
private $variantChildren;

/**
* @var mixed
* @var VariantEntityInterface|Page|Email|DynamicContent|null
**/
private $variantParent;

/**
* @var array
* @var array<string>
*/
private $variantSettingsKeys = ['weight', 'winnerCriteria'];

/**
* @var array<string>
*/
private $parentSettingsKeys = ['totalWeight', 'enableAbTest', 'winnerCriteria', 'sendWinnerDelay'];

/**
* @var array<int|bool|string>
*/
private $variantSettings = [];
private $variantSettings = ['totalWeight' => AbTestSettingsService::DEFAULT_AB_WEIGHT, 'enableAbTest' => false];

/**
* @var \DateTimeInterface|null
Expand All @@ -43,6 +57,7 @@ protected static function addVariantMetadata(ClassMetadataBuilder $builder, $ent
->setIndexBy('id')
->setOrderBy(['isPublished' => 'DESC'])
->mappedBy('variantParent')
->cascadePersist()
->build();

$builder->createField('variantSettings', 'array')
Expand All @@ -64,7 +79,7 @@ protected static function addVariantMetadata(ClassMetadataBuilder $builder, $ent
public function addVariantChild(VariantEntityInterface $child)
{
if (!$this->variantChildren->contains($child)) {
$this->variantChildren[] = $child;
$this->variantChildren->add($child);
}

return $this;
Expand Down Expand Up @@ -131,13 +146,19 @@ public function removeVariantParent(): void
*
* @return $this
*/
public function setVariantSettings($variantSettings)
public function setVariantSettings($variantSettings): self
{
if (method_exists($this, 'isChanged')) {
$this->isChanged('variantSettings', $variantSettings);
}

$this->variantSettings = $variantSettings;
$this->variantSettings = [];

foreach ($this->getSettingsKeys() as $key) {
if (array_key_exists($key, $variantSettings)) {
$this->variantSettings[$key] = $variantSettings[$key];
}
}

return $this;
}
Expand All @@ -160,10 +181,7 @@ public function getVariantStartDate()
return $this->variantStartDate;
}

/**
* @return $this
*/
public function setVariantStartDate($variantStartDate)
public function setVariantStartDate($variantStartDate): self
{
if (method_exists($this, 'isChanged')) {
$this->isChanged('variantStartDate', $variantStartDate);
Expand Down Expand Up @@ -191,6 +209,11 @@ public function isVariant($isChild = false)
}
}

public function isParent(): bool
{
return $this->isVariant() && empty($this->getVariantParent());
}

/**
* Check if this entity has variants.
*/
Expand Down Expand Up @@ -270,6 +293,81 @@ public function getRelatedEntityIds($publishedOnly = false): array
return array_unique($ids);
}

/**
* @return string[]
*/
private function getSettingsKeys()
{
if ($this->getVariantParent()) {
return $this->variantSettingsKeys;
} else {
return $this->parentSettingsKeys;
}
}

public function clearVariantSettings(): void
{
if (!$this->getVariantParent()) {
$this->variantSettings = [
'enableAbTest' => false,
'totalWeight' => AbTestSettingsService::DEFAULT_AB_WEIGHT,
];
} else {
$this->variantSettings = [];
}
}

public function isEnableAbTest(): bool
{
if ($this->getVariantParent()) {
return (bool) ($this->getVariantParent()->getVariantSettings()['enableAbTest'] ?? false);
}

return (bool) ($this->getVariantSettings()['enableAbTest'] ?? false);
}

public function getVariantsPendingCount(int $pendingCount): int
{
if (!$this->isEnableAbTest()) {
return $pendingCount;
}

$pendingCount = $pendingCount + (int) $this->getVariantSentCount(true);

$totalWeight = $this->variantSettings['totalWeight'];
if ($this->getVariantParent()) {
$totalWeight = $this->getVariantParent()->getVariantSettings()['totalWeight'];
}
$totalWeight = (int) ($totalWeight ?? AbTestSettingsService::DEFAULT_TOTAL_WEIGHT);

$variants = $this->getVariantChildren();
$variantCount = count($variants) + 1;
$singleVariantCount = (int) ceil(($pendingCount / $variantCount) * ($totalWeight / 100));

return $singleVariantCount * $variantCount;
}

public function getVariantEndDate(): ?\DateTime
{
/** @var \DateTime $startDate */
$startDate = $this->getVariantStartDate();
$delayHours = $this->getSendWinnerDelay();

if (null === $startDate || null === $delayHours) {
return null;
}

$endDate = clone $startDate;
$endDate->modify("+$delayHours hours");

return $endDate;
}

private function getSendWinnerDelay(): ?int
{
return (int) ($this->getVariantSettings()['sendWinnerDelay'] ?? null);
}

/**
* @return mixed
*/
Expand Down
3 changes: 2 additions & 1 deletion app/bundles/CoreBundle/Event/DetermineWinnerEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@

/**
* @param array{
* parent?: \Mautic\PageBundle\Entity\Page|\Mautic\EmailBundle\Entity\Email,
* factory?: \Mautic\CoreBundle\Factory\MauticFactory,
* parent?: \Mautic\PageBundle\Entity\Page|\Mautic\EmailBundle\Entity\Email|mixed,
* children?: array<mixed>,
* page?: \Mautic\PageBundle\Entity\Page,
* email?: \Mautic\EmailBundle\Entity\Email
* } $parameters
*/
public function __construct(

Check failure on line 27 in app/bundles/CoreBundle/Event/DetermineWinnerEvent.php

View workflow job for this annotation

GitHub Actions / PHPSTAN - 8.2

Parameter $parameters of method Mautic\CoreBundle\Event\DetermineWinnerEvent::__construct() has typehint with deprecated class Mautic\CoreBundle\Factory\MauticFactory: 2.0 to be removed in 3.0
private array $parameters,
) {
}
Expand Down
84 changes: 84 additions & 0 deletions app/bundles/CoreBundle/Model/AbTest/AbTestResultService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Mautic\CoreBundle\Model\AbTest;

use Mautic\CoreBundle\Entity\VariantEntityInterface;
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
use Mautic\CoreBundle\Factory\MauticFactory;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
* Class AbTestResultService.
*/
class AbTestResultService
{
private EventDispatcherInterface $dispatcher;

/**
* @var MauticFactory
*/
private $factory;

/**
* AbTestResultService constructor.
*/
public function __construct(MauticFactory $factory, EventDispatcherInterface $dispatcher)

Check failure on line 25 in app/bundles/CoreBundle/Model/AbTest/AbTestResultService.php

View workflow job for this annotation

GitHub Actions / PHPSTAN - 8.2

Parameter $factory of method Mautic\CoreBundle\Model\AbTest\AbTestResultService::__construct() has typehint with deprecated class Mautic\CoreBundle\Factory\MauticFactory: 2.0 to be removed in 3.0
{
$this->factory = $factory;
$this->dispatcher = $dispatcher;
}

/**
* @param array<mixed>|null $criteria
*
* @return array|mixed
*
* @throws \ReflectionException
*/
public function getAbTestResult(VariantEntityInterface $parentVariant, $criteria)
{
// get A/B test information
[$parent, $children] = $parentVariant->getVariants();

$abTestResults = [];
if (isset($criteria)) {
$testSettings = $criteria;
$args = [
'factory' => $this->factory,
'email' => $parentVariant,
'parent' => $parent,
'children' => $children,
];

if (isset($testSettings['event'])) {
$determineWinnerEvent = new DetermineWinnerEvent($args);

Check failure on line 54 in app/bundles/CoreBundle/Model/AbTest/AbTestResultService.php

View workflow job for this annotation

GitHub Actions / PHPSTAN - 8.2

Parameter #1 $parameters of class Mautic\CoreBundle\Event\DetermineWinnerEvent constructor expects array{factory?: Mautic\CoreBundle\Factory\MauticFactory, parent?: mixed, children?: array<mixed>, page?: Mautic\PageBundle\Entity\Page, email?: Mautic\EmailBundle\Entity\Email}, array{factory: Mautic\CoreBundle\Factory\MauticFactory, email: Mautic\CoreBundle\Entity\VariantEntityInterface, parent: mixed, children: mixed} given.
$this->dispatcher->dispatch($determineWinnerEvent, $testSettings['event']);
$abTestResults = $determineWinnerEvent->getAbTestResults();
}

// execute the callback
if (isset($testSettings['callback']) && is_callable($testSettings['callback'])) {
if (is_array($testSettings['callback'])) {
$reflection = new \ReflectionMethod($testSettings['callback'][0], $testSettings['callback'][1]);
} elseif (false !== strpos($testSettings['callback'], '::')) {
$parts = explode('::', $testSettings['callback']);
$reflection = new \ReflectionMethod($parts[0], $parts[1]);
} else {
$reflection = new \ReflectionMethod('', $testSettings['callback']);
}

$pass = [];
foreach ($reflection->getParameters() as $param) {
if (isset($args[$param->getName()])) {
$pass[] = $args[$param->getName()];
} else {
$pass[] = null;
}
}
$abTestResults = $reflection->invokeArgs($this, $pass);
}
}

return $abTestResults;
}
}
Loading
Loading