Passed
Push — multiple-schemas-support ( 638643 )
by Damien
08:07
created

CleanAuditLogsCommand::collectAuditableEntities()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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