Passed
Push — master ( 0d19a8...30814c )
by Damien
04:12
created

CleanAuditLogsCommand   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 103
c 1
b 0
f 0
dl 0
loc 182
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
A setAuditor() 0 5 1
A unlock() 0 3 1
D execute() 0 147 16
A configure() 0 9 1
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
        if (is_numeric($keep)) {
72
            $deprecationMessage = "Providing an integer value for the 'keep' argument is deprecated. Please use the ISO 8601 duration format (e.g. P12M).";
73
            @trigger_error($deprecationMessage, E_USER_DEPRECATED);
74
            $io->writeln($deprecationMessage);
75
76
            if ((int) $keep <= 0) {
77
                $io->error("'keep' argument must be a positive number.");
78
                $this->release();
79
80
                return 0;
81
            }
82
83
            $until = new DateTime();
84
            $until->modify('-'.$keep.' month');
85
        } else {
86
            try {
87
                $dateInterval = new DateInterval((string) $keep);
88
            } catch (Exception $e) {
89
                $io->error(sprintf("'keep' argument must be a valid ISO 8601 date interval. '%s' given.", (string) $keep));
90
                $this->release();
91
92
                return 0;
93
            }
94
95
            $until = new DateTime();
96
            $until->sub($dateInterval);
97
        }
98
99
        /** @var DoctrineProvider $provider */
100
        $provider = $this->auditor->getProvider(DoctrineProvider::class);
101
        $schemaManager = new SchemaManager($provider);
102
103
//        $entities = $this->provider->getConfiguration()->getEntities();
104
        /** @var StorageService[] $storageServices */
105
        $storageServices = $provider->getStorageServices();
106
107
        // auditable entities by storage entity manager
108
        $repository = [];
109
        $count = 0;
110
111
        // Collect auditable entities from auditing storage managers
112
        /** @var AuditingService[] $auditingServices */
113
        $auditingServices = $provider->getAuditingServices();
114
        foreach ($auditingServices as $name => $auditingService) {
115
            $classes = $schemaManager->getAuditableTableNames($auditingService->getEntityManager());
116
            // Populate the auditable entities repository
117
            foreach ($classes as $entity => $tableName) {
118
                $storageService = $provider->getStorageServiceForEntity($entity);
119
                $key = array_search($storageService, $provider->getStorageServices(), true);
120
                if (!isset($repository[$key])) {
121
                    $repository[$key] = [];
122
                }
123
                $repository[$key][$entity] = $tableName;
124
                ++$count;
125
            }
126
        }
127
128
        $message = sprintf(
129
            "You are about to clean audits created before <comment>%s</comment>: %d entities involved.\n Do you want to proceed?",
130
            $until->format('Y-m-d'),
131
            $count
132
        );
133
134
        $confirm = $input->getOption('no-confirm') ? true : $io->confirm($message, false);
135
        $dryRun = (bool) $input->getOption('dry-run');
0 ignored issues
show
Unused Code introduced by
The assignment to $dryRun is dead and can be removed.
Loading history...
136
        $dumpSQL = (bool) $input->getOption('dump-sql');
137
138
        if ($confirm) {
139
            /** @var Configuration $configuration */
140
            $configuration = $provider->getConfiguration();
141
142
            $progressBar = new ProgressBar($output, $count);
143
            $progressBar->setBarWidth(70);
144
            $progressBar->setFormat("%message%\n".$progressBar->getFormatDefinition('debug'));
145
146
            $progressBar->setMessage('Starting...');
147
            $progressBar->start();
148
149
            $queries = [];
150
            foreach ($repository as $name => $entities) {
151
                foreach ($entities as $entity => $tablename) {
152
                    $auditTable = preg_replace(
153
                        sprintf('#^([^\.]+\.)?(%s)$#', preg_quote($tablename, '#')),
154
                        sprintf(
155
                            '$1%s$2%s',
156
                            preg_quote($configuration->getTablePrefix(), '#'),
157
                            preg_quote($configuration->getTableSuffix(), '#')
158
                        ),
159
                        $tablename
160
                    );
161
162
                    /**
163
                     * @var QueryBuilder
164
                     */
165
                    $queryBuilder = $storageServices[$name]->getEntityManager()->getConnection()->createQueryBuilder();
166
                    $queryBuilder
167
                        ->delete($auditTable)
168
                        ->where('created_at < :until')
169
                        ->setParameter(':until', $until->format('Y-m-d'))
170
                    ;
171
172
                    if ($dumpSQL) {
173
                        $queries[] = str_replace(':until', "'".$until->format('Y-m-d')."'", $queryBuilder->getSQL());
174
                    } else {
175
                        $queryBuilder->execute();
176
                    }
177
178
                    $progressBar->setMessage("Cleaning audit tables... (<info>{$auditTable}</info>)");
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 0;
206
    }
207
}
208