Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}