Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user