Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Dispatcher\FieldColumnBackgroundJobDispatcher;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Exception\ColumnAlreadyCreatedException;
|
||||
use Mautic\LeadBundle\Field\Exception\CustomFieldLimitException;
|
||||
use Mautic\LeadBundle\Field\Exception\LeadFieldWasNotFoundException;
|
||||
use Mautic\LeadBundle\Field\Notification\CustomFieldNotification;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
|
||||
class BackgroundService
|
||||
{
|
||||
public function __construct(
|
||||
private FieldModel $fieldModel,
|
||||
private CustomFieldColumn $customFieldColumn,
|
||||
private LeadFieldSaver $leadFieldSaver,
|
||||
private LeadFieldDeleter $leadFieldDeleter,
|
||||
private FieldColumnBackgroundJobDispatcher $fieldColumnBackgroundJobDispatcher,
|
||||
private CustomFieldNotification $customFieldNotification,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnCreateException
|
||||
* @throws ColumnAlreadyCreatedException
|
||||
* @throws CustomFieldLimitException
|
||||
* @throws LeadFieldWasNotFoundException
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws DriverException
|
||||
* @throws SchemaException
|
||||
* @throws \Mautic\CoreBundle\Exception\SchemaException
|
||||
*/
|
||||
public function addColumn(int $leadFieldId, ?int $userId): void
|
||||
{
|
||||
$leadField = $this->fieldModel->getEntity($leadFieldId);
|
||||
if (null === $leadField) {
|
||||
throw new LeadFieldWasNotFoundException('LeadField entity was not found');
|
||||
}
|
||||
|
||||
if (!$leadField->getColumnIsNotCreated()) {
|
||||
$this->customFieldNotification->customFieldWasCreated($leadField, $userId);
|
||||
throw new ColumnAlreadyCreatedException('Column was already created');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fieldColumnBackgroundJobDispatcher->dispatchPreAddColumnEvent($leadField);
|
||||
} catch (NoListenerException $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$this->customFieldColumn->processCreateLeadColumn($leadField, false);
|
||||
} catch (DriverException|SchemaException|\Mautic\CoreBundle\Exception\SchemaException $e) {
|
||||
$this->customFieldNotification->customFieldCannotBeCreated($leadField, $userId);
|
||||
throw $e;
|
||||
} catch (CustomFieldLimitException $e) {
|
||||
$this->customFieldNotification->customFieldLimitWasHit($leadField, $userId);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$leadField->setColumnWasCreated();
|
||||
$this->leadFieldSaver->saveLeadFieldEntity($leadField, false);
|
||||
|
||||
$this->customFieldNotification->customFieldWasCreated($leadField, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws DriverException
|
||||
* @throws LeadFieldWasNotFoundException
|
||||
* @throws SchemaException
|
||||
* @throws \Mautic\CoreBundle\Exception\SchemaException
|
||||
*/
|
||||
public function updateColumn(int $leadFieldId, int $userId): void
|
||||
{
|
||||
$leadField = $this->fieldModel->getEntity($leadFieldId);
|
||||
if (null === $leadField) {
|
||||
throw new LeadFieldWasNotFoundException('LeadField entity was not found');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fieldColumnBackgroundJobDispatcher->dispatchPreUpdateColumnEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Update the column length of leads table.
|
||||
$this->customFieldColumn->processUpdateLeadColumnLength($leadField);
|
||||
} catch (\Mautic\CoreBundle\Exception\SchemaException|\OutOfRangeException $e) {
|
||||
$this->customFieldNotification->customFieldCannotBeUpdated($leadField, $userId);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->customFieldColumn->processUpdateLeadColumn($leadField);
|
||||
$this->customFieldNotification->customFieldWasUpdated($leadField, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws DriverException
|
||||
* @throws LeadFieldWasNotFoundException
|
||||
* @throws SchemaException
|
||||
* @throws \Mautic\CoreBundle\Exception\SchemaException
|
||||
*/
|
||||
public function deleteColumn(int $leadFieldId, int $userId): void
|
||||
{
|
||||
$leadField = $this->fieldModel->getEntity($leadFieldId);
|
||||
if (null === $leadField) {
|
||||
throw new LeadFieldWasNotFoundException('LeadField entity was not found');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fieldColumnBackgroundJobDispatcher->dispatchPreDeleteColumnEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
|
||||
$this->customFieldColumn->processDeleteLeadColumn($leadField);
|
||||
$this->leadFieldDeleter->deleteLeadFieldEntity($leadField, true);
|
||||
$this->customFieldNotification->customFieldWasDeleted($leadField, $userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Command;
|
||||
|
||||
use Mautic\LeadBundle\Field\SchemaDefinition;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
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:fields:analyse',
|
||||
description: 'Analyse actual usage of custom columns in leads table.'
|
||||
)]
|
||||
class AnalyseCustomFieldCommand extends Command
|
||||
{
|
||||
public function __construct(private FieldModel $fieldModel, private LeadModel $leadModel, private TranslatorInterface $translator)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'display-table',
|
||||
't',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display results in table format'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$displayAsTable = $input->getOption('display-table');
|
||||
|
||||
$fieldDetails = $this->getCustomFieldDetails();
|
||||
if (empty($fieldDetails)) {
|
||||
$output->writeln('No custom field(s) to analyse!!!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$results = $this->leadModel->getCustomLeadFieldLength(array_keys($fieldDetails));
|
||||
|
||||
$fieldLengths = [];
|
||||
foreach ($results as $key => $detail) {
|
||||
$fieldLengths[$key] = ['max_length' => $detail];
|
||||
}
|
||||
|
||||
$analysisDetails = array_merge_recursive($fieldDetails, $fieldLengths);
|
||||
|
||||
$headers = [
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.name'),
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.alias'),
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.length'),
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.max_length'),
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.suggested_length'),
|
||||
$this->translator->trans('mautic.lead.field.analyse.header.indexed'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
foreach ($analysisDetails as $analysisDetail) {
|
||||
$maxLength = (int) $analysisDetail['max_length'] ?: 0;
|
||||
$columnLength = (int) $analysisDetail['char_length_limit'] ?: 0;
|
||||
$suggestedMaxSize = $this->getSuggestedMaxSize($columnLength, $maxLength);
|
||||
|
||||
$label = $analysisDetail['label'];
|
||||
$rows[] = [
|
||||
"\"$label\"",
|
||||
$analysisDetail['alias'],
|
||||
$columnLength,
|
||||
$maxLength,
|
||||
$suggestedMaxSize,
|
||||
$analysisDetail['is_index'] ? $this->translator->trans('mautic.core.yes') : $this->translator->trans('mautic.core.no'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($displayAsTable) {
|
||||
$table = new Table($output);
|
||||
$table
|
||||
->setHeaders($headers)
|
||||
->setRows($rows);
|
||||
$table->render();
|
||||
} else {
|
||||
$output->writeln(implode(', ', $headers));
|
||||
foreach ($rows as $row) {
|
||||
$output->writeln(implode(', ', $row));
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function getCustomFieldDetails(): array
|
||||
{
|
||||
$fields = $this->fieldModel->getLeadFieldCustomFields();
|
||||
$fieldSchemas = $this->fieldModel->getLeadFieldCustomFieldSchemaDetails();
|
||||
|
||||
$fieldDetails = [];
|
||||
foreach ($fields as $field) {
|
||||
if (!isset($fieldSchemas[$field->getAlias()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaDef = SchemaDefinition::getSchemaDefinition($field->getAlias(), $field->getType(), $field->getIsUniqueIdentifier(), $field->getCharLengthLimit());
|
||||
if ('string' !== $schemaDef['type']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldDetails[$field->getAlias()] = [
|
||||
'label' => $field->getLabel(),
|
||||
'alias' => $field->getAlias(),
|
||||
'type' => $schemaDef['type'],
|
||||
'char_length_limit' => $fieldSchemas[$field->getAlias()]->getLength(),
|
||||
'is_index' => $field->isIsIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
return $fieldDetails;
|
||||
}
|
||||
|
||||
private function getSuggestedMaxSize(int $columnLength, int $utilisedLength): int
|
||||
{
|
||||
if ($utilisedLength > 0) {
|
||||
if (191 < $utilisedLength) {
|
||||
return $columnLength;
|
||||
}
|
||||
|
||||
return min($utilisedLength * 2, $columnLength, 191);
|
||||
}
|
||||
|
||||
return min($columnLength, 191);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Command;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Mautic\CoreBundle\Command\ModeratedCommand;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Field\BackgroundService;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\ColumnAlreadyCreatedException;
|
||||
use Mautic\LeadBundle\Field\Exception\CustomFieldLimitException;
|
||||
use Mautic\LeadBundle\Field\Exception\LeadFieldWasNotFoundException;
|
||||
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: CreateCustomFieldCommand::COMMAND_NAME,
|
||||
description: 'Create custom field column in the background',
|
||||
)]
|
||||
class CreateCustomFieldCommand extends ModeratedCommand
|
||||
{
|
||||
public const COMMAND_NAME = 'mautic:custom-field:create-column';
|
||||
|
||||
public function __construct(
|
||||
private BackgroundService $backgroundService,
|
||||
private TranslatorInterface $translator,
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
PathsHelper $pathsHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($pathsHelper, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->addOption('--id', '-i', InputOption::VALUE_OPTIONAL, 'LeadField ID.')
|
||||
->addOption('--user', '-u', InputOption::VALUE_OPTIONAL, 'User ID - User which receives a notification.')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command will create columns in a lead_fields table if the process should run in background.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$leadFieldId = (int) $input->getOption('id');
|
||||
$userId = (int) $input->getOption('user');
|
||||
|
||||
if ($leadFieldId) {
|
||||
return $this->addColumn($leadFieldId, $userId, $input, $output);
|
||||
}
|
||||
|
||||
return $this->addAllMissingColumns($input, $output);
|
||||
}
|
||||
|
||||
private function addAllMissingColumns(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$hasNoErrors = Command::SUCCESS;
|
||||
while ($leadField = $this->leadFieldRepository->getFieldThatIsMissingColumn()) {
|
||||
if (Command::FAILURE === $this->addColumn($leadField->getId(), $leadField->getCreatedBy(), $input, $output)) {
|
||||
$hasNoErrors = Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasNoErrors;
|
||||
}
|
||||
|
||||
private function addColumn(int $leadFieldId, ?int $userId, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$moderationKey = sprintf('%s-%s-%s', self::COMMAND_NAME, $leadFieldId, $userId);
|
||||
|
||||
if (!$this->checkRunStatus($input, $output, $moderationKey)) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->backgroundService->addColumn($leadFieldId, $userId);
|
||||
} catch (LeadFieldWasNotFoundException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.notfound').'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
} catch (ColumnAlreadyCreatedException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.column_already_created').'</error>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (AbortColumnCreateException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.column_creation_aborted').'</error>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (CustomFieldLimitException|DriverException|SchemaException|\Doctrine\DBAL\Exception|\Mautic\CoreBundle\Exception\SchemaException $e) {
|
||||
$output->writeln('<error>'.$this->translator->trans($e->getMessage()).'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln('<info>'.$this->translator->trans('mautic.lead.field.column_was_created', ['%id%' => $leadFieldId]).'</info>');
|
||||
$this->completeRun();
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Command;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Field\BackgroundService;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Exception\LeadFieldWasNotFoundException;
|
||||
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;
|
||||
|
||||
final class DeleteCustomFieldCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private BackgroundService $backgroundService,
|
||||
private TranslatorInterface $translator,
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this->setName('mautic:custom-field:delete-column')
|
||||
->setDescription('Delete custom field column in the background')
|
||||
->addOption('--id', '-i', InputOption::VALUE_REQUIRED, 'LeadField ID.')
|
||||
->addOption('--user', '-u', InputOption::VALUE_OPTIONAL, 'User ID - User which receives a notification.')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command will delete a column in a lead_fields table if the proces should run in background.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$leadFieldId = (int) $input->getOption('id');
|
||||
$userId = (int) $input->getOption('user');
|
||||
|
||||
// Field ID wasn't provided. Try to find a field that is marked for deletion.
|
||||
if (!$leadFieldId) {
|
||||
/** @var ?LeadField $field */
|
||||
$field = $this->leadFieldRepository->findOneBy(['columnIsNotRemoved' => true]);
|
||||
|
||||
if ($field) {
|
||||
$output->writeln('<info>'.$this->translator->trans(
|
||||
'mautic.lead.field.column_was_found_for_deletion',
|
||||
['%fieldName%' => $field->getName(), '%fieldId%' => $field->getId()]
|
||||
).'</info>');
|
||||
|
||||
$leadFieldId = $field->getId();
|
||||
|
||||
if (!$userId && $field->getModifiedBy()) {
|
||||
$userId = $field->getModifiedBy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->backgroundService->deleteColumn($leadFieldId, $userId);
|
||||
} catch (LeadFieldWasNotFoundException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.notfound').'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
} catch (AbortColumnUpdateException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.column_delete_aborted').'</error>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (DriverException|SchemaException|\Mautic\CoreBundle\Exception\SchemaException $e) {
|
||||
$output->writeln('<error>'.$this->translator->trans($e->getMessage()).'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>'.$this->translator->trans('mautic.lead.field.column_was_deleted').'</info>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Command;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ModifyCustomFieldCommand extends Command
|
||||
{
|
||||
public function __construct(private FieldModel $fieldModel, private TranslatorInterface $translator)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('mautic:fields:modify')
|
||||
->setDescription('Change the sizes of the fields')
|
||||
->addArgument(
|
||||
'csv-path',
|
||||
InputArgument::REQUIRED,
|
||||
'Path to a CSV file containing alteration details.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$csvPath = $input->getArgument('csv-path');
|
||||
|
||||
try {
|
||||
$inputCsv = new \SplFileObject($csvPath);
|
||||
} catch (\RuntimeException|\LogicException $e) {
|
||||
$output->writeln(sprintf('<error>Could not open file "%s" because of error "%s".</error>', $csvPath, $e->getMessage()));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$fieldData = $this->convertCsvToArray($inputCsv);
|
||||
|
||||
$fieldsNeedsToBeUpdated = [];
|
||||
foreach ($fieldData as $field) {
|
||||
if ($field['length'] === $field['suggested_length']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($field['suggested_length'] < 1 || $field['suggested_length'] > LeadField::MAX_VARCHAR_LENGTH) {
|
||||
$output->writeln(sprintf('<comment>Skipping "%s", the suggested length must be between 1 and %s.</comment>', $field['name'], LeadField::MAX_VARCHAR_LENGTH));
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldsNeedsToBeUpdated[$field['alias']] = $field;
|
||||
}
|
||||
|
||||
if (empty($fieldsNeedsToBeUpdated)) {
|
||||
$output->writeln('<info>No custom field(s) to update!!!</info>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$lists = $this->getCustomFieldsByAliases(array_keys($fieldsNeedsToBeUpdated));
|
||||
|
||||
foreach ($lists as $field) {
|
||||
$field->setCharLengthLimit((int) $fieldsNeedsToBeUpdated[$field->getAlias()]['suggested_length']);
|
||||
}
|
||||
|
||||
$this->fieldModel->saveEntities($lists);
|
||||
|
||||
$output->writeln(sprintf('<info>%s Field(s) updated successfully.</info>', count($fieldsNeedsToBeUpdated)));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function convertCsvToArray(\SplFileObject $inputCsv): array
|
||||
{
|
||||
$inputCsv->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE);
|
||||
$headerSkipped = false;
|
||||
$keys = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($inputCsv as $row) {
|
||||
if (false === $row) {
|
||||
// skip the last empty row
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = array_map('trim', $row);
|
||||
|
||||
// skip the first(header) row
|
||||
if (!$headerSkipped) {
|
||||
$headerSkipped = true;
|
||||
$keys = $this->getRowKeys($row);
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[] = array_combine($keys, $row);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $aliases
|
||||
*
|
||||
* @return LeadField[]
|
||||
*/
|
||||
private function getCustomFieldsByAliases(array $aliases): array
|
||||
{
|
||||
$filters = [
|
||||
[
|
||||
'column' => 'f.object',
|
||||
'expr' => 'like',
|
||||
'value' => 'lead',
|
||||
],
|
||||
[
|
||||
'column' => 'f.alias',
|
||||
'expr' => 'in',
|
||||
'value' => $aliases,
|
||||
],
|
||||
];
|
||||
$args = [
|
||||
'filter' => [
|
||||
'force' => $filters,
|
||||
],
|
||||
'ignore_paginator' => true,
|
||||
];
|
||||
|
||||
return $this->fieldModel->getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $row
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function getRowKeys(array $row): array
|
||||
{
|
||||
$headers = [
|
||||
'name' => $this->translator->trans('mautic.lead.field.analyse.header.name'),
|
||||
'alias' => $this->translator->trans('mautic.lead.field.analyse.header.alias'),
|
||||
'length' => $this->translator->trans('mautic.lead.field.analyse.header.length'),
|
||||
'max_length' => $this->translator->trans('mautic.lead.field.analyse.header.max_length'),
|
||||
'suggested_length' => $this->translator->trans('mautic.lead.field.analyse.header.suggested_length'),
|
||||
'isIndexed' => $this->translator->trans('mautic.lead.field.analyse.header.indexed'),
|
||||
];
|
||||
|
||||
return array_map(fn ($val) => array_search($val, $headers), $row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Command;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Mautic\LeadBundle\Field\BackgroundService;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Exception\LeadFieldWasNotFoundException;
|
||||
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:custom-field:update-column',
|
||||
description: 'Create custom field column in the background'
|
||||
)]
|
||||
class UpdateCustomFieldCommand extends Command
|
||||
{
|
||||
public function __construct(private BackgroundService $backgroundService, private TranslatorInterface $translator)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
parent::configure();
|
||||
|
||||
$this
|
||||
->addOption('--id', '-i', InputOption::VALUE_REQUIRED, 'LeadField ID.')
|
||||
->addOption('--user', '-u', InputOption::VALUE_OPTIONAL, 'User ID - User which receives a notification.')
|
||||
->setHelp(
|
||||
<<<'EOT'
|
||||
The <info>%command.name%</info> command will create a column in a lead_fields table if the proces should run in background.
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$leadFieldId = (int) $input->getOption('id');
|
||||
$userId = (int) $input->getOption('user');
|
||||
|
||||
try {
|
||||
$this->backgroundService->updateColumn($leadFieldId, $userId);
|
||||
} catch (LeadFieldWasNotFoundException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.notfound').'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
} catch (AbortColumnUpdateException) {
|
||||
$output->writeln('<error>'.$this->translator->trans('mautic.lead.field.column_update_aborted').'</error>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (DriverException|SchemaException|DBALException|\Mautic\CoreBundle\Exception\SchemaException $e) {
|
||||
$output->writeln('<error>'.$this->translator->trans($e->getMessage()).'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>'.$this->translator->trans('mautic.lead.field.column_was_updated').'</info>');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Mautic\CoreBundle\Doctrine\Helper\ColumnSchemaHelper;
|
||||
use Mautic\CoreBundle\Exception\SchemaException;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Dispatcher\FieldColumnDispatcher;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Exception\CustomFieldLimitException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CustomFieldColumn
|
||||
{
|
||||
public function __construct(
|
||||
private ColumnSchemaHelper $columnSchemaHelper,
|
||||
private SchemaDefinition $schemaDefinition,
|
||||
private LoggerInterface $logger,
|
||||
private LeadFieldSaver $leadFieldSaver,
|
||||
private CustomFieldIndex $customFieldIndex,
|
||||
private FieldColumnDispatcher $fieldColumnDispatcher,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnCreateException
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws CustomFieldLimitException
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws DriverException
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function createLeadColumn(LeadField $leadField): void
|
||||
{
|
||||
$leadsSchema = $this->columnSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
// We have to check if the LeadField entity is new and the column already exists .
|
||||
// In such case we must throw an exception to warn users that the column already exists.
|
||||
try {
|
||||
$columnExists = $leadsSchema->checkColumnExists($leadField->getAlias(), $leadField->isNew());
|
||||
|
||||
if ($columnExists && $this->customFieldIndex->isUpdatePending($leadField)) {
|
||||
try {
|
||||
$this->fieldColumnDispatcher->dispatchPreUpdateColumnEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
$this->processUpdateLeadColumn($leadField);
|
||||
}
|
||||
|
||||
if ($columnExists) {
|
||||
return;
|
||||
}
|
||||
} catch (SchemaException) {
|
||||
// We use slightly different error message if the column already exists in this case.
|
||||
throw new SchemaException($this->translator->trans('mautic.lead.field.column.already.exists', ['%field%' => $leadField->getName()], 'validators'));
|
||||
}
|
||||
|
||||
try {
|
||||
$this->fieldColumnDispatcher->dispatchPreAddColumnEvent($leadField);
|
||||
} catch (AbortColumnCreateException $e) {
|
||||
// Save the field metadata and throw the exception again to stop column creation.
|
||||
// As the column should be created by a background job.
|
||||
$this->leadFieldSaver->saveLeadFieldEntityWithoutColumnCreated($leadField);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->processCreateLeadColumn($leadField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the field as its own column in the leads table.
|
||||
*
|
||||
* @throws CustomFieldLimitException
|
||||
* @throws DriverException
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function processCreateLeadColumn(LeadField $leadField, bool $saveLeadField = true): void
|
||||
{
|
||||
$leadsSchema = $this->columnSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
// Check if column do not exist. This method could be called from plugins too.
|
||||
if ($leadsSchema->checkColumnExists($leadField->getAlias())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schemaDefinition = $this->schemaDefinition->getSchemaDefinitionNonStatic(
|
||||
$leadField->getAlias(),
|
||||
$leadField->getType(),
|
||||
(bool) $leadField->getIsUniqueIdentifier(),
|
||||
(int) $leadField->getCharLengthLimit()
|
||||
);
|
||||
|
||||
$leadsSchema->addColumn($schemaDefinition);
|
||||
|
||||
try {
|
||||
$leadsSchema->executeChanges();
|
||||
} catch (DriverException $e) {
|
||||
$this->logger->warning($e->getMessage());
|
||||
|
||||
if (1118 === $e->getCode() /* ER_TOO_BIG_ROWSIZE */) {
|
||||
throw new CustomFieldLimitException('mautic.lead.field.max_column_error');
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($saveLeadField) {
|
||||
// $leadField is a new entity (this is not executed for update), it was successfully added to the lead table > save it
|
||||
$this->leadFieldSaver->saveLeadFieldEntity($leadField, true);
|
||||
}
|
||||
|
||||
if ($leadField->isIsIndex() || $leadField->getIsUniqueIdentifier()) {
|
||||
$this->customFieldIndex->addIndexOnColumn($leadField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the field column in the leads table.
|
||||
*
|
||||
* @throws DriverException
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function processUpdateLeadColumn(LeadField $leadField): void
|
||||
{
|
||||
$hasIndex = $this->customFieldIndex->hasIndex($leadField);
|
||||
|
||||
if ($leadField->isIsIndex() && !$hasIndex) {
|
||||
$this->customFieldIndex->addIndexOnColumn($leadField);
|
||||
} elseif (!$leadField->isIsIndex() && $hasIndex) {
|
||||
$this->customFieldIndex->dropIndexOnColumn($leadField);
|
||||
}
|
||||
|
||||
$this->customFieldIndex->updateUniqueIdentifierIndex($leadField);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function updateLeadColumn(LeadField $leadField): void
|
||||
{
|
||||
try {
|
||||
$this->fieldColumnDispatcher->dispatchPreUpdateColumnEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
} catch (AbortColumnUpdateException) { // if processing in background
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processUpdateLeadColumn($leadField);
|
||||
|
||||
$this->processUpdateLeadColumnLength($leadField);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function processUpdateLeadColumnLength(LeadField $leadField): void
|
||||
{
|
||||
$leadsSchema = $this->columnSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
$leadsSchema->updateColumnLength($leadField->getAlias(), $leadField->getCharLengthLimit());
|
||||
|
||||
$leadsSchema->executeChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a lead field to be deleted.
|
||||
*
|
||||
* @throws \Doctrine\DBAL\Exception
|
||||
* @throws DriverException
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
*/
|
||||
public function deleteLeadColumn(LeadField $leadField): void
|
||||
{
|
||||
try {
|
||||
$this->fieldColumnDispatcher->dispatchPreDeleteColumnEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
} catch (AbortColumnUpdateException) { // if processing in background
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processDeleteLeadColumn($leadField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the field column in the leads table.
|
||||
*
|
||||
* @throws DriverException
|
||||
* @throws \Doctrine\DBAL\Schema\SchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function processDeleteLeadColumn(LeadField $leadField): void
|
||||
{
|
||||
$leadField->deletedId = $leadField->getId();
|
||||
switch ($leadField->getObject()) {
|
||||
case 'lead':
|
||||
$this->columnSchemaHelper->setName('leads')->dropColumn($leadField->getAlias())->executeChanges();
|
||||
break;
|
||||
case 'company':
|
||||
$this->columnSchemaHelper->setName('companies')->dropColumn($leadField->getAlias())->executeChanges();
|
||||
break;
|
||||
}
|
||||
|
||||
$this->columnSchemaHelper->dropColumn($leadField->getCustomFieldObject());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Doctrine\DBAL\Exception\DriverException;
|
||||
use Doctrine\DBAL\Schema\SchemaException as DoctrineSchemaException;
|
||||
use Mautic\CoreBundle\Doctrine\Helper\IndexSchemaHelper;
|
||||
use Mautic\CoreBundle\Exception\SchemaException;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class CustomFieldIndex
|
||||
{
|
||||
public function __construct(
|
||||
private IndexSchemaHelper $indexSchemaHelper,
|
||||
private LoggerInterface $logger,
|
||||
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the unique_identifier_search index and add an index for this field.
|
||||
*
|
||||
* @throws DriverException
|
||||
* @throws DoctrineSchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function addIndexOnColumn(LeadField $leadField): void
|
||||
{
|
||||
try {
|
||||
/** @var IndexSchemaHelper $modifySchema */
|
||||
$modifySchema = $this->indexSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
$alias = $leadField->getAlias();
|
||||
|
||||
$modifySchema->addIndex([$alias], $alias.'_search');
|
||||
$modifySchema->allowColumn($alias);
|
||||
|
||||
$this->updateUniqueIdentifierIndex($leadField);
|
||||
|
||||
$modifySchema->executeChanges();
|
||||
} catch (DriverException $e) {
|
||||
if (1069 === $e->getCode() /* ER_TOO_MANY_KEYS */) {
|
||||
$this->logger->warning($e->getMessage());
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the index for this field.
|
||||
*
|
||||
* @throws DriverException
|
||||
* @throws DoctrineSchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function dropIndexOnColumn(LeadField $leadField): void
|
||||
{
|
||||
try {
|
||||
/** @var IndexSchemaHelper $modifySchema */
|
||||
$modifySchema = $this->indexSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
$alias = $leadField->getAlias();
|
||||
|
||||
$modifySchema->dropIndex([$alias], $alias.'_search');
|
||||
$modifySchema->allowColumn($alias);
|
||||
|
||||
$modifySchema->executeChanges();
|
||||
} catch (DriverException $e) {
|
||||
if (1069 === $e->getCode() /* ER_TOO_MANY_KEYS */) {
|
||||
$this->logger->warning($e->getMessage());
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function isUpdatePending(LeadField $leadField): bool
|
||||
{
|
||||
$hasIndex = $this->hasIndex($leadField);
|
||||
|
||||
if ($leadField->isIsIndex() !== $hasIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->hasMatchingUniqueIdentifierIndex($leadField)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function hasIndex(LeadField $leadField): bool
|
||||
{
|
||||
return $this->indexSchemaHelper->hasIndex($leadField);
|
||||
}
|
||||
|
||||
public function hasMatchingUniqueIdentifierIndex(LeadField $leadField): bool
|
||||
{
|
||||
$uniqueIdentifierColumns = $this->getUniqueIdentifierIndexColumns($leadField->getObject());
|
||||
|
||||
try {
|
||||
return $this->indexSchemaHelper->hasMatchingUniqueIdentifierIndex($leadField, $uniqueIdentifierColumns);
|
||||
} catch (DoctrineSchemaException) {
|
||||
// Return true only if there are no unique identifier fields but otherwise assume the index is missing
|
||||
return 0 === count($uniqueIdentifierColumns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DoctrineSchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function updateUniqueIdentifierIndex(LeadField $leadField): void
|
||||
{
|
||||
if ($this->hasMatchingUniqueIdentifierIndex($leadField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var IndexSchemaHelper $modifySchema */
|
||||
$modifySchema = $this->indexSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
$indexColumns = $this->getUniqueIdentifierIndexColumns($leadField->getObject());
|
||||
if (!$indexColumns) {
|
||||
$this->dropIndexForUniqueIdentifiers($leadField);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$modifySchema->addIndex($indexColumns, 'unique_identifier_search');
|
||||
$modifySchema->executeChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DoctrineSchemaException
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function dropIndexForUniqueIdentifiers(LeadField $leadField): void
|
||||
{
|
||||
if (!$this->indexSchemaHelper->hasUniqueIdentifierIndex($leadField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var IndexSchemaHelper $modifySchema */
|
||||
$modifySchema = $this->indexSchemaHelper->setName($leadField->getCustomFieldObject());
|
||||
|
||||
$indexColumns = $this->getUniqueIdentifierIndexColumns($leadField->getObject());
|
||||
$modifySchema->dropIndex($indexColumns, 'unique_identifier_search');
|
||||
|
||||
$modifySchema->executeChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function getUniqueIdentifierIndexColumns(string $object = 'lead'): array
|
||||
{
|
||||
// Filters
|
||||
$filters = ['object' => $object];
|
||||
|
||||
// Get list of current uniques
|
||||
$uniqueIdentifierFields = $this->fieldsWithUniqueIdentifier->getLiveFields($filters);
|
||||
|
||||
// Always use email
|
||||
$indexColumns = array_keys($uniqueIdentifierFields);
|
||||
|
||||
// Only use three to prevent max key length errors
|
||||
return array_slice($indexColumns, 0, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\DTO;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Exception\InvalidObjectTypeException;
|
||||
|
||||
class CustomFieldObject
|
||||
{
|
||||
private array $objects = [
|
||||
'lead' => 'leads',
|
||||
'company' => 'companies',
|
||||
];
|
||||
|
||||
private LeadField $leadField;
|
||||
|
||||
/**
|
||||
* @throws InvalidObjectTypeException
|
||||
*/
|
||||
public function __construct(LeadField $leadField)
|
||||
{
|
||||
$leadFieldObject = $leadField->getObject();
|
||||
if (!isset($this->objects[$leadFieldObject])) {
|
||||
throw new InvalidObjectTypeException($leadFieldObject.' has no associated object.');
|
||||
}
|
||||
|
||||
$this->leadField = $leadField;
|
||||
}
|
||||
|
||||
public function getObject(): string
|
||||
{
|
||||
return $this->objects[$this->leadField->getObject()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Dispatcher;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Event\AddColumnBackgroundEvent;
|
||||
use Mautic\LeadBundle\Field\Event\DeleteColumnBackgroundEvent;
|
||||
use Mautic\LeadBundle\Field\Event\UpdateColumnBackgroundEvent;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class FieldColumnBackgroundJobDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnCreateException
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreAddColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$action = LeadEvents::LEAD_FIELD_PRE_ADD_COLUMN_BACKGROUND_JOB;
|
||||
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
$event = new AddColumnBackgroundEvent($leadField);
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
if ($event->isPropagationStopped()) {
|
||||
throw new AbortColumnCreateException('Column cannot be created now');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreUpdateColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$action = LeadEvents::LEAD_FIELD_PRE_UPDATE_COLUMN_BACKGROUND_JOB;
|
||||
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
$event = new UpdateColumnBackgroundEvent($leadField);
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
if ($event->isPropagationStopped()) {
|
||||
throw new AbortColumnUpdateException('Column cannot be updated now');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreDeleteColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$action = LeadEvents::LEAD_FIELD_PRE_DELETE_COLUMN_BACKGROUND_JOB;
|
||||
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
$event = new DeleteColumnBackgroundEvent($leadField);
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
if ($event->isPropagationStopped()) {
|
||||
throw new AbortColumnUpdateException('Column cannot be deleted now');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Dispatcher;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Event\AddColumnEvent;
|
||||
use Mautic\LeadBundle\Field\Event\DeleteColumnEvent;
|
||||
use Mautic\LeadBundle\Field\Event\UpdateColumnEvent;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnCreateException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Settings\BackgroundSettings;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class FieldColumnDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private BackgroundSettings $backgroundSettings,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnCreateException
|
||||
*/
|
||||
public function dispatchPreAddColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$shouldProcessInBackground = $this->backgroundSettings->shouldProcessColumnChangeInBackground();
|
||||
$event = new AddColumnEvent($leadField, $shouldProcessInBackground);
|
||||
|
||||
$this->dispatcher->dispatch($event, LeadEvents::LEAD_FIELD_PRE_ADD_COLUMN);
|
||||
|
||||
if ($shouldProcessInBackground) {
|
||||
throw new AbortColumnCreateException('Column change will be processed in background job');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreUpdateColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$action = LeadEvents::LEAD_FIELD_PRE_UPDATE_COLUMN;
|
||||
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
$shouldProcessInBackground = $this->backgroundSettings->shouldProcessColumnChangeInBackground();
|
||||
$event = new UpdateColumnEvent($leadField, $shouldProcessInBackground);
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
if ($event->shouldProcessInBackground()) {
|
||||
throw new AbortColumnUpdateException('Column change will be processed in background job');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AbortColumnUpdateException
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreDeleteColumnEvent(LeadField $leadField): void
|
||||
{
|
||||
$action = LeadEvents::LEAD_FIELD_PRE_DELETE_COLUMN;
|
||||
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
$shouldProcessInBackground = $this->backgroundSettings->shouldProcessColumnChangeInBackground();
|
||||
|
||||
$event = new DeleteColumnEvent($leadField, $shouldProcessInBackground);
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
if ($event->shouldProcessInBackground()) {
|
||||
throw new AbortColumnUpdateException('Column delete will be processed in background job');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Dispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Event\LeadFieldEvent;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
use Mautic\LeadBundle\Field\Settings\BackgroundSettings;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class FieldDeleteDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private EntityManager $entityManager,
|
||||
private BackgroundSettings $backgroundSettings,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoListenerException
|
||||
* @throws AbortColumnUpdateException
|
||||
*/
|
||||
public function dispatchPreDeleteEvent(LeadField $entity): LeadFieldEvent
|
||||
{
|
||||
if ($this->backgroundSettings->shouldProcessColumnChangeInBackground()) {
|
||||
throw new AbortColumnUpdateException('Column change will be processed in background job');
|
||||
}
|
||||
|
||||
return $this->dispatchEvent(LeadEvents::FIELD_PRE_DELETE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPostDeleteEvent(LeadField $entity): LeadFieldEvent
|
||||
{
|
||||
return $this->dispatchEvent(LeadEvents::FIELD_POST_DELETE, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action - Use constant from LeadEvents class (e.g. LeadEvents::FIELD_PRE_SAVE)
|
||||
*
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
private function dispatchEvent($action, LeadField $entity, ?LeadFieldEvent $event = null): LeadFieldEvent
|
||||
{
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for this event');
|
||||
}
|
||||
|
||||
if (null === $event) {
|
||||
$event = new LeadFieldEvent($entity);
|
||||
$event->setEntityManager($this->entityManager);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Dispatcher;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Event\LeadFieldEvent;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class FieldSaveDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private EntityManager $entityManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPreSaveEvent(LeadField $entity, bool $isNew): LeadFieldEvent
|
||||
{
|
||||
return $this->dispatchEvent(LeadEvents::FIELD_PRE_SAVE, $entity, $isNew);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchPostSaveEvent(LeadField $entity, bool $isNew): LeadFieldEvent
|
||||
{
|
||||
return $this->dispatchEvent(LeadEvents::FIELD_POST_SAVE, $entity, $isNew);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoListenerException
|
||||
*/
|
||||
public function dispatchEvent(string $action, LeadField $entity, bool $isNew, ?LeadFieldEvent $event = null): LeadFieldEvent
|
||||
{
|
||||
if (!$this->dispatcher->hasListeners($action)) {
|
||||
throw new NoListenerException('There is no Listener for '.$action.' event');
|
||||
}
|
||||
|
||||
if (null === $event) {
|
||||
$event = new LeadFieldEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->entityManager);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $action);
|
||||
|
||||
return $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class AddColumnBackgroundEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private LeadField $leadField,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class AddColumnEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private LeadField $leadField,
|
||||
private bool $shouldProcessInBackground,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
|
||||
public function shouldProcessInBackground(): bool
|
||||
{
|
||||
return $this->shouldProcessInBackground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class DeleteColumnBackgroundEvent extends Event
|
||||
{
|
||||
public function __construct(private LeadField $leadField)
|
||||
{
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class DeleteColumnEvent extends Event
|
||||
{
|
||||
public function __construct(private LeadField $leadField, private bool $shouldProcessInBackground)
|
||||
{
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
|
||||
public function shouldProcessInBackground(): bool
|
||||
{
|
||||
return $this->shouldProcessInBackground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class UpdateColumnBackgroundEvent extends Event
|
||||
{
|
||||
public function __construct(private LeadField $leadField)
|
||||
{
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class UpdateColumnEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private LeadField $leadField,
|
||||
private bool $shouldProcessInBackground,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getLeadField(): LeadField
|
||||
{
|
||||
return $this->leadField;
|
||||
}
|
||||
|
||||
public function shouldProcessInBackground(): bool
|
||||
{
|
||||
return $this->shouldProcessInBackground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class AbortColumnCreateException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class AbortColumnUpdateException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class ColumnAlreadyCreatedException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class CustomFieldLimitException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class LeadFieldWasNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Exception;
|
||||
|
||||
class NoUserException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class FieldList
|
||||
{
|
||||
public function __construct(
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $filters
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getFieldList(bool $byGroup = true, bool $alphabetical = true, array $filters = ['isPublished' => true, 'object' => 'lead']): array
|
||||
{
|
||||
$forceFilters = [];
|
||||
foreach ($filters as $col => $val) {
|
||||
$forceFilters[] = [
|
||||
'column' => "f.{$col}",
|
||||
'expr' => 'eq',
|
||||
'value' => $val,
|
||||
];
|
||||
}
|
||||
// Get a list of custom form fields
|
||||
$fields = $this->leadFieldRepository->getEntities([
|
||||
'filter' => [
|
||||
'force' => $forceFilters,
|
||||
],
|
||||
'orderBy' => 'f.order',
|
||||
'orderByDir' => 'asc',
|
||||
'result_cache' => new ResultCacheOptions(LeadField::CACHE_NAMESPACE),
|
||||
]);
|
||||
|
||||
$leadFields = [];
|
||||
|
||||
foreach ($fields as $f) {
|
||||
if ($byGroup) {
|
||||
$fieldName = $this->translator->trans('mautic.lead.field.group.'.$f->getGroup());
|
||||
$leadFields[$fieldName][$f->getAlias()] = $f->getLabel();
|
||||
} else {
|
||||
$leadFields[$f->getAlias()] = $f->getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
if ($alphabetical) {
|
||||
// Sort the groups
|
||||
uksort($leadFields, 'strnatcmp');
|
||||
|
||||
if ($byGroup) {
|
||||
// Sort each group by translation
|
||||
foreach ($leadFields as &$fieldGroup) {
|
||||
uasort($fieldGroup, 'strnatcmp');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $leadFields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
class FieldsWithUniqueIdentifier
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $uniqueIdentifierFields = [];
|
||||
|
||||
public function __construct(
|
||||
private FieldList $fieldList,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of cached published fields that are unique identifiers.
|
||||
*
|
||||
* @param array<mixed> $filters
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getFieldsWithUniqueIdentifier(array $filters = []): array
|
||||
{
|
||||
$filters = $this->prepareFilters($filters);
|
||||
|
||||
$key = base64_encode(json_encode($filters));
|
||||
if (!isset($this->uniqueIdentifierFields[$key])) {
|
||||
$this->uniqueIdentifierFields[$key] = $this->fieldList->getFieldList(false, true, $filters);
|
||||
}
|
||||
|
||||
return $this->uniqueIdentifierFields[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of published fields that are unique identifiers fresh from the DB each time.
|
||||
*
|
||||
* @param array<mixed> $filters
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function getLiveFields(array $filters = []): array
|
||||
{
|
||||
$filters = $this->prepareFilters($filters);
|
||||
|
||||
return $this->fieldList->getFieldList(false, true, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $filters
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function prepareFilters(array $filters): array
|
||||
{
|
||||
$filters['isPublished'] ??= true;
|
||||
$filters['isUniqueIdentifer'] ??= true;
|
||||
$filters['object'] ??= 'lead';
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Helper;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
/**
|
||||
* Helper for getting and counting indexes on lead table.
|
||||
*
|
||||
* @see Lead
|
||||
*/
|
||||
class IndexHelper
|
||||
{
|
||||
public const MAX_COUNT_ALLOWED = 64;
|
||||
/**
|
||||
* @var bool|array<string>
|
||||
*/
|
||||
private $indexedColumns = false;
|
||||
|
||||
/**
|
||||
* Can be different from indexed column count when using multiple indexes on same table.
|
||||
*/
|
||||
private int $indexCount = 0;
|
||||
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>|bool
|
||||
*/
|
||||
public function getIndexedColumnNames()
|
||||
{
|
||||
$this->getIndexes();
|
||||
|
||||
return $this->indexedColumns;
|
||||
}
|
||||
|
||||
public function getIndexCount(): int
|
||||
{
|
||||
$this->getIndexes();
|
||||
|
||||
return $this->indexCount;
|
||||
}
|
||||
|
||||
public function getMaxCount(): int
|
||||
{
|
||||
return self::MAX_COUNT_ALLOWED;
|
||||
}
|
||||
|
||||
public function isNewIndexAllowed(): bool
|
||||
{
|
||||
return $this->getIndexCount() < $this->getMaxCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexes created on `leads` table.
|
||||
*
|
||||
* @see Lead
|
||||
*
|
||||
* @throws DBALException
|
||||
*/
|
||||
private function getIndexes(): void
|
||||
{
|
||||
if (false !== $this->indexedColumns) {
|
||||
// Query below performed
|
||||
return;
|
||||
}
|
||||
|
||||
$tableName = $this->entityManager->getClassMetadata(Lead::class)->getTableName();
|
||||
|
||||
$sql = "SHOW INDEXES FROM `$tableName`";
|
||||
|
||||
$stmt = $this->entityManager->getConnection()->prepare($sql);
|
||||
$indexes = $stmt->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$this->indexedColumns = array_map(
|
||||
fn ($index) => $index['Column_name'],
|
||||
$indexes
|
||||
);
|
||||
|
||||
$this->indexCount = count($indexes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Company;
|
||||
use Mautic\LeadBundle\Entity\IdentifierFieldEntityInterface;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class IdentifierFields
|
||||
{
|
||||
public function __construct(
|
||||
private FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
|
||||
private FieldList $fieldList,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFieldList(string $object, ?object $entityClass = null): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->getDefaultFields($object, $entityClass),
|
||||
$this->getUniqueIdentifierFields($object),
|
||||
$this->getSocialFields($object)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getDefaultFields(string $object, ?object $entityClass): array
|
||||
{
|
||||
if (null === $entityClass) {
|
||||
switch ($object) {
|
||||
case 'lead':
|
||||
$entityClass = Lead::class;
|
||||
break;
|
||||
case 'company':
|
||||
$entityClass = Company::class;
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (is_subclass_of($entityClass, IdentifierFieldEntityInterface::class)) {
|
||||
return $entityClass::getDefaultIdentifierFields();
|
||||
}
|
||||
|
||||
// The class wasn't recognized or doesn't implement the interface
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getUniqueIdentifierFields(string $object): array
|
||||
{
|
||||
$fields = $this->fieldsWithUniqueIdentifier->getFieldsWithUniqueIdentifier(
|
||||
[
|
||||
'object' => $object,
|
||||
]
|
||||
);
|
||||
|
||||
return array_keys($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getSocialFields(string $object): array
|
||||
{
|
||||
$fields = $this->fieldList->getFieldList(
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'isPublished' => true,
|
||||
'object' => $object,
|
||||
]
|
||||
);
|
||||
|
||||
if (!isset($fields['Social'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_keys($fields['Social']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Dispatcher\FieldDeleteDispatcher;
|
||||
use Mautic\LeadBundle\Field\Exception\AbortColumnUpdateException;
|
||||
|
||||
class LeadFieldDeleter
|
||||
{
|
||||
public function __construct(
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
private FieldDeleteDispatcher $fieldDeleteDispatcher,
|
||||
private UserHelper $userHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isBackground - if processing in background
|
||||
*/
|
||||
public function deleteLeadFieldEntity(LeadField $leadField, bool $isBackground = false): void
|
||||
{
|
||||
try {
|
||||
$this->fieldDeleteDispatcher->dispatchPreDeleteEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
} catch (AbortColumnUpdateException) { // if processing in background is ON
|
||||
if (!$isBackground) {
|
||||
$this->deleteLeadFieldEntityWithoutColumnRemoved($leadField);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$leadField->deletedId = $leadField->getId();
|
||||
$this->leadFieldRepository->deleteEntity($leadField);
|
||||
|
||||
try {
|
||||
$this->fieldDeleteDispatcher->dispatchPostDeleteEvent($leadField);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the field for delation in the background and sets the modified by user who
|
||||
* will be used as the user who will actually delete the field in the background.
|
||||
* Such soft-deleted field will disappear from the UI.
|
||||
*
|
||||
* Note: The LeadModel would set most of this for us, but cannot be used due to circular dependency.
|
||||
*/
|
||||
private function deleteLeadFieldEntityWithoutColumnRemoved(LeadField $leadField): void
|
||||
{
|
||||
$currentUser = $this->userHelper->getUser();
|
||||
$leadField->setColumnIsNotRemoved();
|
||||
$leadField->setModifiedBy($currentUser);
|
||||
$leadField->setModifiedByUser($currentUser?->getName());
|
||||
$leadField->setDateModified((new DateTimeHelper())->getDateTime());
|
||||
$leadField->setIsPublished(false);
|
||||
|
||||
$this->leadFieldRepository->saveEntity($leadField);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Exception\NoListenerException;
|
||||
use Mautic\LeadBundle\Field\Dispatcher\FieldSaveDispatcher;
|
||||
|
||||
class LeadFieldSaver
|
||||
{
|
||||
public function __construct(
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
private FieldSaveDispatcher $fieldSaveDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function saveLeadFieldEntity(LeadField $leadField, bool $isNew): void
|
||||
{
|
||||
try {
|
||||
$this->fieldSaveDispatcher->dispatchPreSaveEvent($leadField, $isNew);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
|
||||
$this->leadFieldRepository->saveEntity($leadField);
|
||||
|
||||
try {
|
||||
$this->fieldSaveDispatcher->dispatchPostSaveEvent($leadField, $isNew);
|
||||
} catch (NoListenerException) {
|
||||
}
|
||||
}
|
||||
|
||||
public function saveLeadFieldEntityWithoutColumnCreated(LeadField $leadField): void
|
||||
{
|
||||
$leadField->setColumnIsNotCreated();
|
||||
|
||||
$this->saveLeadFieldEntity($leadField, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Notification;
|
||||
|
||||
use Mautic\CoreBundle\Model\NotificationModel;
|
||||
use Mautic\LeadBundle\Entity\LeadField;
|
||||
use Mautic\LeadBundle\Field\Exception\NoUserException;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CustomFieldNotification
|
||||
{
|
||||
public function __construct(
|
||||
private NotificationModel $notificationModel,
|
||||
private UserModel $userModel,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function customFieldWasCreated(LeadField $leadField, ?int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.created_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.created_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
public function customFieldWasUpdated(LeadField $leadField, ?int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.updated_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.updated_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
public function customFieldWasDeleted(LeadField $leadField, int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.deleted_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.deleted_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
public function customFieldLimitWasHit(LeadField $leadField, ?int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.custom_field_limit_hit_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.custom_field_limit_hit_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
public function customFieldCannotBeCreated(LeadField $leadField, ?int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.cannot_be_created_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.cannot_be_created_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
public function customFieldCannotBeUpdated(LeadField $leadField, ?int $userId): void
|
||||
{
|
||||
try {
|
||||
$user = $this->getUser($userId);
|
||||
} catch (NoUserException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $this->translator->trans(
|
||||
'mautic.lead.field.notification.cannot_be_updated_message',
|
||||
['%label%' => $leadField->getLabel()]
|
||||
);
|
||||
$header = $this->translator->trans('mautic.lead.field.notification.cannot_be_updated_header');
|
||||
|
||||
$this->addToNotificationCenter($user, $message, $header);
|
||||
}
|
||||
|
||||
private function addToNotificationCenter(User $user, string $message, string $header): void
|
||||
{
|
||||
$this->notificationModel->addNotification(
|
||||
$message,
|
||||
'info',
|
||||
false,
|
||||
$header,
|
||||
'ri-layout-column-line',
|
||||
null,
|
||||
$user
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoUserException
|
||||
*/
|
||||
private function getUser(?int $userId): User
|
||||
{
|
||||
if (!$userId || !$user = $this->userModel->getEntity($userId)) {
|
||||
throw new NoUserException();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field;
|
||||
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class SchemaDefinition
|
||||
{
|
||||
/**
|
||||
* Max length of VARCHAR fields.
|
||||
* Fields: charLengthLimit.
|
||||
*/
|
||||
public const MAX_VARCHAR_LENGTH = 191;
|
||||
|
||||
/**
|
||||
* Get the MySQL database type based on the field type
|
||||
* Use a static function so that it's accessible from DoctrineSubscriber
|
||||
* without causing a circular service injection error.
|
||||
*/
|
||||
public static function getSchemaDefinition(string $alias, string $type, bool $isUnique = false, ?int $length = null): array
|
||||
{
|
||||
$options = ['notnull' => false];
|
||||
|
||||
// Unique is always a string in order to control index length
|
||||
if ($isUnique) {
|
||||
return [
|
||||
'name' => $alias,
|
||||
'type' => 'string',
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
case 'time':
|
||||
case 'boolean':
|
||||
$schemaType = $type;
|
||||
break;
|
||||
case 'number':
|
||||
$schemaType = 'float';
|
||||
break;
|
||||
case 'timezone':
|
||||
case 'locale':
|
||||
case 'country':
|
||||
case 'email':
|
||||
case 'lookup':
|
||||
case 'select':
|
||||
case 'region':
|
||||
case 'tel':
|
||||
case 'url':
|
||||
$schemaType = 'string';
|
||||
$options['length'] = $length ?: self::MAX_VARCHAR_LENGTH;
|
||||
break;
|
||||
case 'text':
|
||||
$schemaType = (str_contains($alias, 'description')) ? 'text' : 'string';
|
||||
$options['length'] = $length;
|
||||
break;
|
||||
case 'multiselect':
|
||||
$schemaType = 'text';
|
||||
$options['length'] = 65535;
|
||||
break;
|
||||
case 'html':
|
||||
default:
|
||||
$schemaType = 'text';
|
||||
}
|
||||
|
||||
if ('string' === $schemaType && empty($options['length'])) {
|
||||
$options['length'] = self::MAX_VARCHAR_LENGTH;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $alias,
|
||||
'type' => $schemaType,
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $schemaDefinition
|
||||
*/
|
||||
public static function getFieldCharLengthLimit(array $schemaDefinition): ?int
|
||||
{
|
||||
$length = $schemaDefinition['options']['length'] ?? null;
|
||||
$type = $schemaDefinition['type'] ?? null;
|
||||
|
||||
return match ($type) {
|
||||
'string' => $length ?? ClassMetadataBuilder::MAX_VARCHAR_INDEXED_LENGTH,
|
||||
'text' => $length,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MySQL database type based on the field type.
|
||||
*/
|
||||
public function getSchemaDefinitionNonStatic(string $alias, string $type, bool $isUnique = false, ?int $length = null): array
|
||||
{
|
||||
return self::getSchemaDefinition($alias, $type, $isUnique, $length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\LeadBundle\Field\Settings;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class BackgroundSettings
|
||||
{
|
||||
public const CREATE_CUSTOM_FIELD_IN_BACKGROUND = 'create_custom_field_in_background';
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function shouldProcessColumnChangeInBackground(): bool
|
||||
{
|
||||
return (bool) $this->coreParametersHelper->get(self::CREATE_CUSTOM_FIELD_IN_BACKGROUND, false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user