Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Entity\LeadEventLogRepository;
use Mautic\CampaignBundle\Model\CampaignModel;
use Mautic\CampaignBundle\Model\EventModel;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: CampaignDeleteEventLogsCommand::COMMAND_NAME,
description: 'Delete campaign event logs'
)]
class CampaignDeleteEventLogsCommand extends Command
{
public const COMMAND_NAME = 'mautic:campaign:delete-event-logs';
public function __construct(private LeadEventLogRepository $leadEventLogRepository, private CampaignModel $campaignModel, private EventModel $eventModel)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument(
'campaign_event_ids',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
'Campaign event ids to delete event logs.'
)
->addOption(
'--campaign-id',
'-i',
InputOption::VALUE_OPTIONAL,
'Delete campaign also otherwise will delete event and event log only.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$eventIds = $input->getArgument('campaign_event_ids');
$campaignId = (int) $input->getOption('campaign-id');
if (!empty($campaignId)) {
$this->leadEventLogRepository->removeEventLogsByCampaignId($campaignId);
$this->eventModel->deleteEventsByCampaignId($campaignId);
$campaign = $this->campaignModel->getEntity($campaignId);
$this->campaignModel->deleteCampaign($campaign);
} elseif (!empty($eventIds)) {
$this->leadEventLogRepository->removeEventLogs($eventIds);
$this->eventModel->deleteEventsByEventIds($eventIds);
}
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:campaigns:execute',
description: 'Execute specific scheduled events.'
)]
class ExecuteEventCommand extends Command
{
use WriteCountTrait;
public function __construct(
private ScheduledExecutioner $scheduledExecutioner,
private TranslatorInterface $translator,
private FormatterHelper $formatterHelper,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption(
'--scheduled-log-ids',
null,
InputOption::VALUE_REQUIRED,
'CSV of specific scheduled log IDs to execute.'
)
->addOption(
'--execution-time',
null,
InputOption::VALUE_REQUIRED,
'Scheduled execution time of event log'
);
parent::configure();
}
/**
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1);
$now = empty($input->getOption('execution-time')) ? null : new \DateTime($input->getOption('execution-time'));
$ids = $this->formatterHelper->simpleCsvToArray($input->getOption('scheduled-log-ids'), 'int');
$counter = $this->scheduledExecutioner->executeByIds($ids, $output, $now);
$this->writeCounts($output, $this->translator, $counter);
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Model\SummaryModel;
use Mautic\CoreBundle\Command\ModeratedCommand;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: SummarizeCommand::NAME,
description: 'Builds historical campaign summary statistics if they do not already exist.'
)]
class SummarizeCommand extends ModeratedCommand
{
use WriteCountTrait;
public const NAME = 'mautic:campaigns:summarize';
public function __construct(
private TranslatorInterface $translator,
private SummaryModel $summaryModel,
PathsHelper $pathsHelper,
CoreParametersHelper $coreParametersHelper,
) {
parent::__construct($pathsHelper, $coreParametersHelper);
}
protected function configure(): void
{
$this
->addOption(
'--batch-limit',
'-l',
InputOption::VALUE_OPTIONAL,
'Number of hours to process per batch. 1 hour is default value.',
'1'
)
->addOption(
'--max-hours',
null,
InputOption::VALUE_OPTIONAL,
'Optionally specify how many hours back in time you wish to summarize.'
)
->addOption(
'--rebuild',
null,
InputOption::VALUE_NONE,
'Rebuild existing data. To be used only if database exceptions have been known to cause inaccuracies.'
);
parent::configure();
}
/**
* @throws \Doctrine\DBAL\Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkRunStatus($input, $output)) {
return \Symfony\Component\Console\Command\Command::SUCCESS;
}
$batchLimit = (int) $input->getOption('batch-limit');
$maxHours = (int) $input->getOption('max-hours');
$rebuild = (bool) $input->getOption('rebuild');
$output->writeln(
"<info>{$this->translator->trans('mautic.campaign.summarizing', ['%batch%' => $batchLimit])}</info>"
);
$this->summaryModel->summarize($output, $batchLimit, $maxHours, $rebuild);
$this->completeRun();
return \Symfony\Component\Console\Command\Command::SUCCESS;
}
}

View File

@@ -0,0 +1,429 @@
<?php
namespace Mautic\CampaignBundle\Command;
use Exception;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\CampaignRepository;
use Mautic\CampaignBundle\Event\CampaignTriggerEvent;
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
use Mautic\CampaignBundle\Executioner\InactiveExecutioner;
use Mautic\CampaignBundle\Executioner\KickoffExecutioner;
use Mautic\CampaignBundle\Executioner\ScheduledExecutioner;
use Mautic\CoreBundle\Command\ModeratedCommand;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\ExitCode;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\ProcessSignal\Exception\SignalCaughtException;
use Mautic\CoreBundle\ProcessSignal\ProcessSignalService;
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
use Mautic\LeadBundle\Model\ListModel;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:campaigns:trigger',
description: 'Trigger timed events for published campaigns.'
)]
class TriggerCampaignCommand extends ModeratedCommand
{
use WriteCountTrait;
private bool $kickoffOnly = false;
private bool $inactiveOnly = false;
private bool $scheduleOnly = false;
/**
* @var OutputInterface
*/
protected $output;
private ?ContactLimiter $limiter = null;
private ?Campaign $campaign = null;
public function __construct(
private CampaignRepository $campaignRepository,
private EventDispatcherInterface $dispatcher,
private TranslatorInterface $translator,
private KickoffExecutioner $kickoffExecutioner,
private ScheduledExecutioner $scheduledExecutioner,
private InactiveExecutioner $inactiveExecutioner,
private LoggerInterface $logger,
private FormatterHelper $formatterHelper,
private ListModel $listModel,
private SegmentCountCacheHelper $segmentCountCacheHelper,
PathsHelper $pathsHelper,
private CoreParametersHelper $coreParametersHelper,
private ProcessSignalService $processSignalService,
) {
parent::__construct($pathsHelper, $coreParametersHelper);
}
protected function configure()
{
$this
->addOption(
'--campaign-id',
'-i',
InputOption::VALUE_OPTIONAL,
'Trigger events for a specific campaign. Otherwise, all campaigns will be triggered.',
null
)
->addOption(
'--campaign-limit',
null,
InputOption::VALUE_OPTIONAL,
'Limit number of contacts on a per campaign basis',
null
)
->addOption(
'--contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Trigger events for a specific contact.',
null
)
->addOption(
'--contact-ids',
null,
InputOption::VALUE_OPTIONAL,
'CSV of contact IDs to evaluate.'
)
->addOption(
'--min-contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Trigger events starting at a specific contact ID.',
null
)
->addOption(
'--max-contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Trigger events starting up to a specific contact ID.',
null
)
->addOption(
'--thread-id',
null,
InputOption::VALUE_OPTIONAL,
'The number of this current process if running multiple in parallel.'
)
->addOption(
'--max-threads',
null,
InputOption::VALUE_OPTIONAL,
'The maximum number of processes you intend to run in parallel.'
)
->addOption(
'--kickoff-only',
null,
InputOption::VALUE_NONE,
'Just kickoff the campaign'
)
->addOption(
'--scheduled-only',
null,
InputOption::VALUE_NONE,
'Just execute scheduled events'
)
->addOption(
'--inactive-only',
null,
InputOption::VALUE_NONE,
'Just execute scheduled events'
)
->addOption(
'--batch-limit',
'-l',
InputOption::VALUE_OPTIONAL,
'Set batch size of contacts to process per round. Defaults to 100.',
100
)
->addOption(
'exclude',
'd',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
'Exclude a specific campaign from being triggered. Otherwise, all campaigns will be triggered.',
[]
);
parent::configure();
}
/**
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$fn = fn (int $signal) => $output->writeln(sprintf('Signal %d caught.', $signal));
$this->processSignalService->registerSignalHandler($fn);
$quiet = $input->getOption('quiet');
$this->output = $quiet ? new NullOutput() : $output;
$this->kickoffOnly = $input->getOption('kickoff-only');
$this->scheduleOnly = $input->getOption('scheduled-only');
$this->inactiveOnly = $input->getOption('inactive-only');
$id = $input->getOption('campaign-id');
$batchLimit = $input->getOption('batch-limit');
$campaignLimit = $input->getOption('campaign-limit');
$contactMinId = $input->getOption('min-contact-id');
$contactMaxId = $input->getOption('max-contact-id');
$contactId = $input->getOption('contact-id');
$contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int');
$threadId = $input->getOption('thread-id');
$maxThreads = $input->getOption('max-threads');
$excludeCampaigns = $input->getOption('exclude');
if (is_numeric($id)) {
$id = (int) $id;
}
if (is_numeric($maxThreads)) {
$maxThreads = (int) $maxThreads;
}
if (is_numeric($threadId)) {
$threadId = (int) $threadId;
}
if (is_numeric($contactMaxId)) {
$contactMaxId = (int) $contactMaxId;
}
if (is_numeric($contactMinId)) {
$contactMinId = (int) $contactMinId;
}
if (is_numeric($contactId)) {
$contactId = (int) $contactId;
}
if (is_numeric($campaignLimit)) {
$campaignLimit = (int) $campaignLimit;
}
if ($threadId && $maxThreads && (int) $threadId > (int) $maxThreads) {
$this->output->writeln('--thread-id cannot be larger than --max-thread');
return \Symfony\Component\Console\Command\Command::FAILURE;
}
$this->limiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreads, $campaignLimit);
defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1);
$moderationKey = sprintf('%s-%s', $id, $threadId);
if (!$this->checkRunStatus($input, $this->output, $moderationKey)) {
return \Symfony\Component\Console\Command\Command::SUCCESS;
}
try {
// Specific campaign;
if ($id) {
$statusCode = ExitCode::SUCCESS;
/** @var Campaign $campaign */
if ($campaign = $this->campaignRepository->getEntity($id)) {
$this->triggerCampaign($campaign);
} else {
$output->writeln('<error>'.$this->translator->trans('mautic.campaign.rebuild.not_found', ['%id%' => $id]).'</error>');
$statusCode = ExitCode::FAILURE;
}
$this->completeRun();
return (int) $statusCode;
}
// All published campaigns
$filter = [
'iterable_mode' => true,
'orderBy' => 'c.dateAdded',
'orderByDir' => 'DESC',
];
// exclude excluded campaigns
if (is_array($excludeCampaigns) && count($excludeCampaigns) > 0) {
$filter['filter'] = [
'force' => [
[
'expr' => 'notIn',
'column' => $this->campaignRepository->getTableAlias().'.id',
'value' => $excludeCampaigns,
],
],
];
}
/** @var \Doctrine\ORM\Internal\Hydration\IterableResult $campaigns */
$campaigns = $this->campaignRepository->getEntities($filter);
foreach ($campaigns as $campaign) {
$this->triggerCampaign($campaign);
if ($this->limiter->hasCampaignLimit()) {
$this->limiter->resetCampaignLimitRemaining();
}
}
$this->completeRun();
return ExitCode::SUCCESS;
} catch (SignalCaughtException) {
$exitCode = ExitCode::TERMINATED;
}
return $exitCode;
}
/**
* @return bool
*/
protected function dispatchTriggerEvent(Campaign $campaign)
{
if ($this->dispatcher->hasListeners(CampaignEvents::CAMPAIGN_ON_TRIGGER)) {
/** @var CampaignTriggerEvent $event */
$event = $this->dispatcher->dispatch(
new CampaignTriggerEvent($campaign),
CampaignEvents::CAMPAIGN_ON_TRIGGER
);
return $event->shouldTrigger();
}
return true;
}
/**
* @throws \Exception
*/
private function triggerCampaign(Campaign $campaign): void
{
if (!$campaign->isPublished()) {
return;
}
if (!$this->dispatchTriggerEvent($campaign)) {
return;
}
$this->campaign = $campaign;
try {
$this->output->writeln('<info>'.$this->translator->trans('mautic.campaign.trigger.triggering', ['%id%' => $campaign->getId()]).'</info>');
// Reset batch limiter
$this->limiter->resetBatchMinContactId();
// Execute starting events
if (!$this->inactiveOnly && !$this->scheduleOnly) {
$this->executeKickoff();
}
// Reset batch limiter
$this->limiter->resetBatchMinContactId();
// Execute scheduled events
if (!$this->inactiveOnly && !$this->kickoffOnly) {
$this->executeScheduled();
}
// Reset batch limiter
$this->limiter->resetBatchMinContactId();
// Execute inactive events
if (!$this->scheduleOnly && !$this->kickoffOnly) {
$this->executeInactive();
}
} catch (SignalCaughtException $e) {
throw $e;
} catch (\Exception $exception) {
if ('prod' !== MAUTIC_ENV) {
// Throw the exception for dev/test mode
throw $exception;
}
$this->logger->error('CAMPAIGN: '.$exception->getMessage());
} finally {
// Update campaign linked segment cache count.
$this->updateCampaignSegmentContactCount($campaign);
}
// Don't detach in tests since this command will be ran multiple times in the same process
if ('test' !== MAUTIC_ENV) {
$this->campaignRepository->detachEntity($campaign);
}
}
/**
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
* @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException
* @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException
*/
private function executeKickoff(): void
{
// trigger starting action events for newly added contacts
$this->output->writeln('<comment>'.$this->translator->trans('mautic.campaign.trigger.starting').'</comment>');
$counter = $this->kickoffExecutioner->execute($this->campaign, $this->limiter, $this->output);
$this->writeCounts($this->output, $this->translator, $counter);
}
/**
* @throws \Doctrine\ORM\Query\QueryException
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
* @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException
* @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException
*/
private function executeScheduled(): void
{
$this->output->writeln('<comment>'.$this->translator->trans('mautic.campaign.trigger.scheduled').'</comment>');
$counter = $this->scheduledExecutioner->execute($this->campaign, $this->limiter, $this->output);
$this->writeCounts($this->output, $this->translator, $counter);
}
/**
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
* @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException
* @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException
*/
private function executeInactive(): void
{
// find and trigger "no" path events
$this->output->writeln('<comment>'.$this->translator->trans('mautic.campaign.trigger.negative').'</comment>');
$counter = $this->inactiveExecutioner->execute($this->campaign, $this->limiter, $this->output);
$this->writeCounts($this->output, $this->translator, $counter);
}
/**
* @throws \Exception
*/
private function updateCampaignSegmentContactCount(Campaign $campaign): void
{
$segmentIds = $this->campaignRepository->getCampaignListIds((int) $campaign->getId());
$updateSegmentCountInBackground = $this->coreParametersHelper->get('update_segment_contact_count_in_background', false);
foreach ($segmentIds as $segmentId) {
if ($updateSegmentCountInBackground) {
$this->segmentCountCacheHelper->invalidateSegmentContactCount($segmentId);
} else {
$totalLeadCount = $this->listModel->getRepository()->getLeadCount($segmentId);
$this->segmentCountCacheHelper->setSegmentContactCount($segmentId, (int) $totalLeadCount);
}
}
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Entity\Campaign;
use Mautic\CampaignBundle\Entity\CampaignRepository;
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
use Mautic\CampaignBundle\Membership\MembershipBuilder;
use Mautic\CoreBundle\Command\ModeratedCommand;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:campaigns:rebuild',
description: 'Rebuild campaigns based on contact segments.',
aliases: ['mautic:campaigns:update']
)]
class UpdateLeadCampaignsCommand extends ModeratedCommand
{
private int $runLimit = 0;
private ContactLimiter $contactLimiter;
private bool $quiet = false;
public function __construct(
private CampaignRepository $campaignRepository,
private TranslatorInterface $translator,
private MembershipBuilder $membershipBuilder,
private LoggerInterface $logger,
private FormatterHelper $formatterHelper,
PathsHelper $pathsHelper,
CoreParametersHelper $coreParametersHelper,
) {
parent::__construct($pathsHelper, $coreParametersHelper);
}
protected function configure()
{
$this
->addOption('--batch-limit', '-l', InputOption::VALUE_OPTIONAL, 'Set batch size of contacts to process per round. Defaults to 300.', 300)
->addOption(
'--max-contacts',
'-m',
InputOption::VALUE_OPTIONAL,
'Set max number of contacts to process per campaign for this script execution. Defaults to all.',
0
)
->addOption(
'--campaign-id',
'-i',
InputOption::VALUE_OPTIONAL,
'Build membership for a specific campaign. Otherwise, all campaigns will be rebuilt.',
null
)
->addOption(
'--contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Build membership for a specific contact.',
null
)
->addOption(
'--contact-ids',
null,
InputOption::VALUE_OPTIONAL,
'CSV of contact IDs to evaluate.'
)
->addOption(
'--min-contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Build membership starting at a specific contact ID.',
null
)
->addOption(
'--max-contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Build membership up to a specific contact ID.',
null
)
->addOption(
'--thread-id',
null,
InputOption::VALUE_OPTIONAL,
'The number of this current process if running multiple in parallel.'
)
->addOption(
'--max-threads',
null,
InputOption::VALUE_OPTIONAL,
'The maximum number of processes you intend to run in parallel.'
)
->addOption(
'exclude',
'd',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
'Exclude a specific campaign from being rebuilt. Otherwise, all campaigns will be rebuilt.',
[]
);
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$id = $input->getOption('campaign-id');
$batchLimit = $input->getOption('batch-limit');
$contactMinId = $input->getOption('min-contact-id');
$contactMaxId = $input->getOption('max-contact-id');
$contactId = $input->getOption('contact-id');
$contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int');
$threadId = $input->getOption('thread-id');
$maxThreads = $input->getOption('max-threads');
$this->runLimit = $input->getOption('max-contacts');
$this->quiet = (bool) $input->getOption('quiet');
$this->output = ($this->quiet) ? new NullOutput() : $output;
$excludeCampaigns = $input->getOption('exclude');
if (is_numeric($id)) {
$id = (int) $id;
}
if (is_numeric($maxThreads)) {
$maxThreads = (int) $maxThreads;
}
if (is_numeric($threadId)) {
$threadId = (int) $threadId;
}
if (is_numeric($contactMaxId)) {
$contactMaxId = (int) $contactMaxId;
}
if (is_numeric($contactMinId)) {
$contactMinId = (int) $contactMinId;
}
if (is_numeric($contactId)) {
$contactId = (int) $contactId;
}
if ($threadId && $maxThreads && (int) $threadId > (int) $maxThreads) {
$this->output->writeln('--thread-id cannot be larger than --max-thread');
return \Symfony\Component\Console\Command\Command::FAILURE;
}
if (!$this->checkRunStatus($input, $output, $id)) {
return \Symfony\Component\Console\Command\Command::SUCCESS;
}
$this->contactLimiter = new ContactLimiter($batchLimit, $contactId, $contactMinId, $contactMaxId, $contactIds, $threadId, $maxThreads);
if ($id) {
$campaign = $this->campaignRepository->getEntity($id);
if (null === $campaign) {
$output->writeln('<error>'.$this->translator->trans('mautic.campaign.rebuild.not_found', ['%id%' => $id]).'</error>');
return \Symfony\Component\Console\Command\Command::FAILURE;
}
$this->updateCampaign($campaign);
} else {
$filter = [
'iterable_mode' => true,
];
if (is_array($excludeCampaigns) && count($excludeCampaigns) > 0) {
$filter['filter'] = [
'force' => [
[
'expr' => 'notIn',
'column' => $this->campaignRepository->getTableAlias().'.id',
'value' => $excludeCampaigns,
],
],
];
}
$campaigns = $this->campaignRepository->getEntities($filter);
foreach ($campaigns as $campaign) {
$this->updateCampaign($campaign);
unset($campaign);
}
}
$this->completeRun();
return \Symfony\Component\Console\Command\Command::SUCCESS;
}
/**
* @throws \Exception
*/
private function updateCampaign(Campaign $campaign): void
{
if (!$campaign->isPublished()) {
return;
}
try {
$this->output->writeln(
'<info>'.$this->translator->trans('mautic.campaign.rebuild.rebuilding', ['%id%' => $campaign->getId()]).'</info>'
);
// Reset batch limiter
$this->contactLimiter->resetBatchMinContactId();
$this->membershipBuilder->build($campaign, $this->contactLimiter, $this->runLimit, ($this->quiet) ? null : $this->output);
} catch (\Exception $exception) {
if ('prod' !== MAUTIC_ENV) {
// Throw the exception for dev/test mode
throw $exception;
}
$this->logger->error('CAMPAIGN: '.$exception->getMessage());
}
// Don't detach in tests since this command will be ran multiple times in the same process
if ('test' !== MAUTIC_ENV) {
$this->campaignRepository->detachEntity($campaign);
}
$this->output->writeln('');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
use Mautic\CampaignBundle\Executioner\InactiveExecutioner;
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand(
name: 'mautic:campaigns:validate',
description: 'Validate if a contact has been inactive for a decision and execute events if so.'
)]
class ValidateEventCommand extends Command
{
use WriteCountTrait;
public function __construct(
private InactiveExecutioner $inactiveExecution,
private TranslatorInterface $translator,
private FormatterHelper $formatterHelper,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption(
'--decision-id',
null,
InputOption::VALUE_REQUIRED,
'ID of the decision to evaluate.'
)
->addOption(
'--contact-id',
null,
InputOption::VALUE_OPTIONAL,
'Evaluate for specific contact'
)
->addOption(
'--contact-ids',
null,
InputOption::VALUE_OPTIONAL,
'CSV of contact IDs to evaluate.'
);
parent::configure();
}
/**
* @throws \Exception
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
defined('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED') or define('MAUTIC_CAMPAIGN_SYSTEM_TRIGGERED', 1);
$decisionId = $input->getOption('decision-id');
$contactId = $input->getOption('contact-id');
if (is_numeric($decisionId)) {
$decisionId = (int) $decisionId;
}
if (is_numeric($contactId)) {
$contactId = (int) $contactId;
}
$contactIds = $this->formatterHelper->simpleCsvToArray($input->getOption('contact-ids'), 'int');
if (!$contactIds && !$contactId) {
$output->writeln(
"\n".
'<comment>'.$this->translator->trans('mautic.campaign.trigger.events_executed', ['%count%' => 0])
.'</comment>'
);
return Command::SUCCESS;
}
$limiter = new ContactLimiter(null, $contactId, null, null, $contactIds);
$counter = $this->inactiveExecution->validate($decisionId, $limiter, $output);
$this->writeCounts($output, $this->translator, $counter);
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Mautic\CampaignBundle\Command;
use Mautic\CampaignBundle\Executioner\Result\Counter;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
trait WriteCountTrait
{
private function writeCounts(OutputInterface $output, TranslatorInterface $translator, Counter $counter): void
{
$output->writeln('');
$output->writeln(
'<comment>'.$translator->trans(
'mautic.campaign.trigger.events_executed',
['%count%' => $counter->getTotalExecuted()]
)
.'</comment>'
);
$output->writeln(
'<comment>'.$translator->trans(
'mautic.campaign.trigger.events_scheduled',
['%count%' => $counter->getTotalScheduled()]
)
.'</comment>'
);
$output->writeln('');
}
}