CleanAuditLogsCommand::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 12
rs 9.9332
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DH\Auditor\Provider\Doctrine\Persistence\Command;
6
7
use DateInterval;
8
use DateTimeImmutable;
9
use DH\Auditor\Auditor;
10
use DH\Auditor\Provider\Doctrine\Configuration;
11
use DH\Auditor\Provider\Doctrine\DoctrineProvider;
12
use DH\Auditor\Provider\Doctrine\Persistence\Schema\SchemaManager;
13
use DH\Auditor\Provider\Doctrine\Service\StorageService;
14
use Exception;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Command\LockableTrait;
17
use Symfony\Component\Console\Helper\ProgressBar;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Console\Style\SymfonyStyle;
23
24
/**
25
 * @see \DH\Auditor\Tests\Provider\Doctrine\Persistence\Command\CleanAuditLogsCommandTest
26
 */
27
final class CleanAuditLogsCommand extends Command
28
{
29
    use LockableTrait;
30
31
    /**
32
     * @var string
33
     */
34
    private const UNTIL_DATE_FORMAT = 'Y-m-d H:i:s';
35
36
    private Auditor $auditor;
37
38
    public function setAuditor(Auditor $auditor): self
39
    {
40
        $this->auditor = $auditor;
41
42
        return $this;
43
    }
44
45
    protected function configure(): void
46
    {
47
        $this
48
            ->setDescription('Cleans audit tables')
49
            ->setName('audit:clean')
50
            ->addOption('no-confirm', null, InputOption::VALUE_NONE, 'No interaction mode')
51
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not execute SQL queries.')
52
            ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Prints SQL related queries.')
53
            ->addOption('exclude', 'x', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Entities to exclude from cleaning')
54
            ->addOption('include', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Entities to include in cleaning')
55
            ->addOption('date', 'd', InputOption::VALUE_REQUIRED, 'Specify a custom date to clean audits until (must be expressed as an ISO 8601 date, e.g. 2023-04-24).')
56
            ->addArgument('keep', InputArgument::OPTIONAL, 'Audits retention period (must be expressed as an ISO 8601 date interval, e.g. P12M to keep the last 12 months or P7D to keep the last 7 days).', 'P12M')
57
        ;
58
    }
59
60
    protected function execute(InputInterface $input, OutputInterface $output): int
61
    {
62
        if (!$this->lock()) {
63
            $output->writeln('The command is already running in another process.');
64
65
            return Command::SUCCESS;
66
        }
67
68
        $io = new SymfonyStyle($input, $output);
69
70
        $keep = $input->getArgument('keep');
71
        $keep = (\is_array($keep) ? $keep[0] : $keep);
72
73
        /** @var ?string $date */
74
        $date = $input->getOption('date');
75
        $until = null;
76
77
        if (null !== $date) {
78
            // Use custom date if provided
79
            try {
80
                $until = new DateTimeImmutable($date);
81
            } catch (Exception) {
82
                $io->error(sprintf('Invalid date format provided: %s', $date));
83
            }
84
        } else {
85
            // Fall back to default retention period
86
            $until = $this->validateKeepArgument($keep, $io);
87
        }
88
89
        if (!$until instanceof DateTimeImmutable) {
90
            return Command::SUCCESS;
91
        }
92
93
        /** @var DoctrineProvider $provider */
94
        $provider = $this->auditor->getProvider(DoctrineProvider::class);
95
        $schemaManager = new SchemaManager($provider);
96
97
        /** @var StorageService[] $storageServices */
98
        $storageServices = $provider->getStorageServices();
99
100
        // auditable classes by storage entity manager
101
        $count = 0;
102
103
        // Collect auditable classes from auditing storage managers
104
        $rawExcludeValues = $input->getOption('exclude') ?? [];
105
        $rawIncludeValues = $input->getOption('include') ?? [];
106
        $excludeEntities = \is_array($rawExcludeValues) ? $rawExcludeValues : [$rawExcludeValues];
107
        $includeEntities = \is_array($rawIncludeValues) ? $rawIncludeValues : [$rawIncludeValues];
108
        $repository = $schemaManager->collectAuditableEntities();
109
        $filteredRepository = [];
110
111
        foreach ($repository as $name => $entityClasses) {
112
            foreach ($entityClasses as $entityClass => $table) {
113
                if (
114
                    !\in_array($entityClass, $excludeEntities, true)
115
                    && (
116
                        [] === $includeEntities
117
                        || \in_array($entityClass, $includeEntities, true)
118
                    )
119
                ) {
120
                    $filteredRepository[$name][$entityClass] = $table;
121
                }
122
            }
123
        }
124
125
        $repository = $filteredRepository;
126
127
        foreach ($repository as $entities) {
128
            $count += \count($entities);
129
        }
130
131
        $message = sprintf(
132
            "You are about to clean audits created before <comment>%s</comment>: %d classes involved.\n Do you want to proceed?",
133
            $until->format(self::UNTIL_DATE_FORMAT),
134
            $count
135
        );
136
137
        $confirm = $input->getOption('no-confirm') ? true : $io->confirm($message, false);
138
        $dryRun = (bool) $input->getOption('dry-run');
139
        $dumpSQL = (bool) $input->getOption('dump-sql');
140
141
        if ($confirm) {
142
            /** @var Configuration $configuration */
143
            $configuration = $provider->getConfiguration();
144
145
            $progressBar = new ProgressBar($output, $count);
146
            $progressBar->setBarWidth(70);
147
            $progressBar->setFormat("%message%\n".$progressBar->getFormatDefinition('debug'));
148
149
            $progressBar->setMessage('Starting...');
150
            $progressBar->start();
151
152
            $queries = [];
153
154
            /**
155
             * @var string                $name
156
             * @var array<string, string> $classes
157
             */
158
            foreach ($repository as $name => $classes) {
159
                foreach (array_keys($classes) as $entity) {
160
                    $connection = $storageServices[$name]->getEntityManager()->getConnection();
161
                    $auditTable = $schemaManager->resolveAuditTableName($entity, $configuration, $connection->getDatabasePlatform());
162
163
                    $queryBuilder = $connection->createQueryBuilder();
164
                    $queryBuilder
165
                        ->delete($auditTable)
166
                        ->where('created_at < :until')
167
                        ->setParameter('until', $until->format(self::UNTIL_DATE_FORMAT))
168
                    ;
169
170
                    if ($dumpSQL) {
171
                        $queries[] = str_replace(':until', "'".$until->format(self::UNTIL_DATE_FORMAT)."'", $queryBuilder->getSQL());
172
                    }
173
174
                    if (!$dryRun) {
175
                        $queryBuilder->executeStatement();
176
                    }
177
178
                    $progressBar->setMessage(sprintf('Cleaning audit tables... (<info>%s</info>)', $auditTable));
179
                    $progressBar->advance();
180
                }
181
            }
182
183
            $progressBar->setMessage('Cleaning audit tables... (<info>done</info>)');
184
            $progressBar->display();
185
186
            $io->newLine();
187
            if ($dumpSQL) {
188
                $io->newLine();
189
                $io->writeln('SQL queries to be run:');
190
                foreach ($queries as $query) {
191
                    $io->writeln($query);
192
                }
193
            }
194
195
            $io->newLine();
196
            $io->success('Success.');
197
        } else {
198
            $io->success('Cancelled.');
199
        }
200
201
        // if not released explicitly, Symfony releases the lock
202
        // automatically when the execution of the command ends
203
        $this->release();
204
205
        return Command::SUCCESS;
206
    }
207
208
    private function validateKeepArgument(string $keep, SymfonyStyle $io): ?DateTimeImmutable
209
    {
210
        try {
211
            $dateInterval = new DateInterval($keep);
212
        } catch (Exception) {
213
            $io->error(sprintf("'keep' argument must be a valid ISO 8601 date interval, '%s' given.", $keep));
214
            $this->release();
215
216
            return null;
217
        }
218
219
        return (new DateTimeImmutable())->sub($dateInterval);
220
    }
221
}
222