Passed
Push — master ( 30814c...959b31 )
by Damien
03:08
created

CleanAuditLogsCommand::validateKeepArgument()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 30
rs 9.6333
cc 4
nc 4
nop 2
1
<?php
2
3
namespace DH\Auditor\Provider\Doctrine\Persistence\Command;
4
5
use DateInterval;
6
use DateTime;
7
use DH\Auditor\Auditor;
8
use DH\Auditor\Provider\Doctrine\Configuration;
9
use DH\Auditor\Provider\Doctrine\DoctrineProvider;
10
use DH\Auditor\Provider\Doctrine\Persistence\Schema\SchemaManager;
11
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
12
use DH\Auditor\Provider\Doctrine\Service\StorageService;
13
use Doctrine\DBAL\Query\QueryBuilder;
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
class CleanAuditLogsCommand extends Command
25
{
26
    use LockableTrait;
27
28
    protected static $defaultName = 'audit:clean';
29
30
    /**
31
     * @var Auditor
32
     */
33
    private $auditor;
34
35
    public function unlock(): void
36
    {
37
        $this->release();
38
    }
39
40
    public function setAuditor(Auditor $auditor): self
41
    {
42
        $this->auditor = $auditor;
43
44
        return $this;
45
    }
46
47
    protected function configure(): void
48
    {
49
        $this
50
            ->setDescription('Cleans audit tables')
51
            ->setName(self::$defaultName)
52
            ->addOption('no-confirm', null, InputOption::VALUE_NONE, 'No interaction mode')
53
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not execute SQL queries.')
54
            ->addOption('dump-sql', null, InputOption::VALUE_NONE, 'Prints SQL related queries.')
55
            ->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')
56
        ;
57
    }
58
59
    protected function execute(InputInterface $input, OutputInterface $output)
60
    {
61
        if (!$this->lock()) {
62
            $output->writeln('The command is already running in another process.');
63
64
            return 0;
65
        }
66
67
        $io = new SymfonyStyle($input, $output);
68
69
        $keep = $input->getArgument('keep');
70
        $keep = (\is_array($keep) ? $keep[0] : $keep);
71
        $until = $this->validateKeepArgument($keep, $io);
1 ignored issue
show
Bug introduced by
It seems like $keep can also be of type null; however, parameter $keep of DH\Auditor\Provider\Doct...:validateKeepArgument() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

71
        $until = $this->validateKeepArgument(/** @scrutinizer ignore-type */ $keep, $io);
Loading history...
72
73
        if (null === $until) {
74
            return 0;
75
        }
76
77
        /** @var DoctrineProvider $provider */
78
        $provider = $this->auditor->getProvider(DoctrineProvider::class);
79
        $schemaManager = new SchemaManager($provider);
80
81
//        $entities = $this->provider->getConfiguration()->getEntities();
82
        /** @var StorageService[] $storageServices */
83
        $storageServices = $provider->getStorageServices();
84
85
        // auditable entities by storage entity manager
86
        $repository = [];
87
        $count = 0;
88
89
        // Collect auditable entities from auditing storage managers
90
        [$repository, $count] = $this->collectAuditableEntities($provider, $schemaManager, $repository, $count);
91
92
        $message = sprintf(
93
            "You are about to clean audits created before <comment>%s</comment>: %d entities involved.\n Do you want to proceed?",
94
            $until->format('Y-m-d'),
95
            $count
96
        );
97
98
        $confirm = $input->getOption('no-confirm') ? true : $io->confirm($message, false);
99
        $dryRun = (bool) $input->getOption('dry-run');
100
        $dumpSQL = (bool) $input->getOption('dump-sql');
101
102
        if ($confirm) {
103
            /** @var Configuration $configuration */
104
            $configuration = $provider->getConfiguration();
105
106
            $progressBar = new ProgressBar($output, $count);
107
            $progressBar->setBarWidth(70);
108
            $progressBar->setFormat("%message%\n".$progressBar->getFormatDefinition('debug'));
109
110
            $progressBar->setMessage('Starting...');
111
            $progressBar->start();
112
113
            $queries = [];
114
            foreach ($repository as $name => $entities) {
115
                foreach ($entities as $entity => $tablename) {
116
                    $auditTable = $this->computeAuditTablename($tablename, $configuration);
117
118
                    /**
119
                     * @var QueryBuilder
120
                     */
121
                    $queryBuilder = $storageServices[$name]->getEntityManager()->getConnection()->createQueryBuilder();
122
                    $queryBuilder
123
                        ->delete($auditTable)
124
                        ->where('created_at < :until')
125
                        ->setParameter(':until', $until->format('Y-m-d'))
126
                    ;
127
128
                    if ($dumpSQL) {
129
                        $queries[] = str_replace(':until', "'".$until->format('Y-m-d')."'", $queryBuilder->getSQL());
130
                    }
131
132
                    if (!$dryRun) {
133
                        $queryBuilder->execute();
134
                    }
135
136
                    $progressBar->setMessage("Cleaning audit tables... (<info>{$auditTable}</info>)");
137
                    $progressBar->advance();
138
                }
139
            }
140
141
            $progressBar->setMessage('Cleaning audit tables... (<info>done</info>)');
142
            $progressBar->display();
143
144
            $io->newLine();
145
            if ($dumpSQL) {
146
                $io->newLine();
147
                $io->writeln('SQL queries to be run:');
148
                foreach ($queries as $query) {
149
                    $io->writeln($query);
150
                }
151
            }
152
153
            $io->newLine();
154
            $io->success('Success.');
155
        } else {
156
            $io->success('Cancelled.');
157
        }
158
159
        // if not released explicitly, Symfony releases the lock
160
        // automatically when the execution of the command ends
161
        $this->release();
162
163
        return 0;
164
    }
165
166
    private function validateKeepArgument(string $keep, SymfonyStyle $io): ?DateTime
167
    {
168
        $until = new DateTime();
169
        if (is_numeric($keep)) {
170
            $deprecationMessage = "Providing an integer value for the 'keep' argument is deprecated. Please use the ISO 8601 duration format (e.g. P12M).";
171
            @trigger_error($deprecationMessage, E_USER_DEPRECATED);
172
            $io->writeln($deprecationMessage);
173
174
            if ((int) $keep <= 0) {
175
                $io->error("'keep' argument must be a positive number.");
176
                $this->release();
177
178
                return null;
179
            }
180
181
            $until->modify('-'.$keep.' month');
182
        } else {
183
            try {
184
                $dateInterval = new DateInterval((string) $keep);
185
            } catch (Exception $e) {
186
                $io->error(sprintf("'keep' argument must be a valid ISO 8601 date interval. '%s' given.", (string) $keep));
187
                $this->release();
188
189
                return null;
190
            }
191
192
            $until->sub($dateInterval);
193
        }
194
195
        return $until;
196
    }
197
198
    private function collectAuditableEntities(DoctrineProvider $provider, SchemaManager $schemaManager, array $repository, int $count): array
199
    {
200
        /** @var AuditingService[] $auditingServices */
201
        $auditingServices = $provider->getAuditingServices();
202
        foreach ($auditingServices as $name => $auditingService) {
203
            $classes = $schemaManager->getAuditableTableNames($auditingService->getEntityManager());
204
            // Populate the auditable entities repository
205
            foreach ($classes as $entity => $tableName) {
206
                $storageService = $provider->getStorageServiceForEntity($entity);
207
                $key = array_search($storageService, $provider->getStorageServices(), true);
208
                if (!isset($repository[$key])) {
209
                    $repository[$key] = [];
210
                }
211
                $repository[$key][$entity] = $tableName;
212
                ++$count;
213
            }
214
        }
215
216
        return [$repository, $count];
217
    }
218
219
    private function computeAuditTablename($tablename, Configuration $configuration): ?string
220
    {
221
        return preg_replace(
222
            sprintf('#^([^\.]+\.)?(%s)$#', preg_quote($tablename, '#')),
223
            sprintf(
224
                '$1%s$2%s',
225
                preg_quote($configuration->getTablePrefix(), '#'),
226
                preg_quote($configuration->getTableSuffix(), '#')
227
            ),
228
            $tablename
229
        );
230
    }
231
}
232