RunCommand   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Test Coverage

Coverage 97.83%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 150
c 3
b 0
f 0
dl 0
loc 277
ccs 135
cts 138
cp 0.9783
rs 10
wmc 25

5 Methods

Rating   Name   Duplication   Size   Complexity  
B configure() 0 68 1
C execute() 0 70 12
A anonymizeTable() 0 39 5
A __construct() 0 6 1
A getTotal() 0 18 6
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * neuralyzer : Data Anonymization Library and CLI Tool
7
 *
8
 * PHP Version 7.2
9
 *
10
 * @author    Emmanuel Dyan
11
 * @author    Rémi Sauvat
12
 *
13
 * @copyright 2020 Emmanuel Dyan
14
 *
15
 * @package edyan/neuralyzer
16
 *
17
 * @license GNU General Public License v2.0
18
 *
19
 * @link https://github.com/edyan/neuralyzer
20
 */
21
22
namespace Edyan\Neuralyzer\Console\Commands;
23
24
use Edyan\Neuralyzer\Anonymizer\DB;
25
use Edyan\Neuralyzer\Configuration\Reader;
26
use Edyan\Neuralyzer\Utils\DBUtils;
27
use Edyan\Neuralyzer\Utils\Expression;
28
use Edyan\Neuralyzer\Utils\FileLoader;
29
use Symfony\Component\Console\Command\Command;
30
use Symfony\Component\Console\Helper\ProgressBar;
31
use Symfony\Component\Console\Input\InputInterface;
32
use Symfony\Component\Console\Input\InputOption;
33
use Symfony\Component\Console\Output\OutputInterface;
34
use Symfony\Component\Console\Question\Question;
35
use Symfony\Component\Stopwatch\Stopwatch;
36
37
/**
38
 * Command to launch an anonymization based on a config file
39
 */
40
class RunCommand extends Command
41
{
42
    /**
43
     * Store the DB Object
44
     *
45
     * @var DB
46
     */
47
    private $db;
48
49
    /**
50
     * Set the command shortcut to be used in configuration
51
     *
52
     * @var string
53
     */
54
    private $command = 'run';
55
56
    /**
57
     * Symfony's Input Class for parameters and options
58
     *
59
     * @var InputInterface
60
     */
61
    private $input;
62
63
    /**
64
     * Symfony's Output Class to display info
65
     *
66
     * @var OutputInterface
67
     */
68
    private $output;
69
70
    /**
71
     * Neuralyzer reader
72
     *
73
     * @var Reader
74
     */
75
    private $reader;
76
77
    /**
78
     * Store the DBUtils Object (autowiring)
79
     *
80
     * @var DBUtils
81
     */
82
    private $dbUtils;
83
84
    /**
85
     * Store the Expression Object (autowiring)
86
     *
87
     * @var Expression
88
     */
89
    private $expression;
90
91
    public function __construct(DBUtils $dbUtils, Expression $expression)
92
    {
93 28
        parent::__construct();
94
95 28
        $this->dbUtils = $dbUtils;
96
        $this->expression = $expression;
97 28
    }
98 28
99 28
    protected function configure(): void
100
    {
101
        // First command : Test the DB Connexion
102
        $this->setName($this->command)
103
            ->setDescription('Run Anonymizer')
104
            ->setHelp(
105
                'This command will connect to a DB and run the anonymizer from a yaml config'.PHP_EOL.
106 28
                "Usage: <info>./bin/neuralyzer {$this->command} -u app -p app -f neuralyzer.yml</info>"
107
            )->addOption(
108
                'driver',
109 28
                'D',
110 28
                InputOption::VALUE_REQUIRED,
111 28
                'Driver (check Doctrine documentation to have the list)',
112 28
                'pdo_mysql'
113 28
            )->addOption(
114 28
                'host',
115 28
                'H',
116 28
                InputOption::VALUE_REQUIRED,
117 28
                'Host',
118 28
                '127.0.0.1'
119 28
            )->addOption(
120 28
                'db',
121 28
                'd',
122 28
                InputOption::VALUE_REQUIRED,
123 28
                'Database Name'
124 28
            )->addOption(
125 28
                'user',
126 28
                'u',
127 28
                InputOption::VALUE_REQUIRED,
128 28
                'User Name',
129 28
                get_current_user()
130 28
            )->addOption(
131 28
                'password',
132 28
                'p',
133 28
                InputOption::VALUE_REQUIRED,
134 28
                'Password (or prompted)'
135 28
            )->addOption(
136 28
                'config',
137 28
                'c',
138 28
                InputOption::VALUE_REQUIRED,
139 28
                'Configuration File',
140 28
                'neuralyzer.yml'
141 28
            )->addOption(
142 28
                'table',
143 28
                't',
144 28
                InputOption::VALUE_REQUIRED,
145 28
                'Do a single table'
146 28
            )->addOption(
147 28
                'pretend',
148 28
                null,
149 28
                InputOption::VALUE_NONE,
150 28
                "Don't run queries (pre and post actions will always be executed)"
151 28
            )->addOption(
152 28
                'sql',
153 28
                's',
154 28
                InputOption::VALUE_NONE,
155 28
                'Display the SQL'
156 28
            )->addOption(
157 28
                'mode',
158 28
                'm',
159 28
                InputOption::VALUE_REQUIRED,
160 28
                'Set the mode : batch or queries',
161 28
                'batch'
162 28
            )->addOption(
163 28
                'bootstrap',
164 28
                'b',
165 28
                InputOption::VALUE_REQUIRED,
166 28
                'Provide a bootstrap file to load a custom setup before executing the command. Format /path/to/bootstrap.php'
167 28
            )
168 28
        ;
169 28
    }
170 28
171 28
    /**
172 28
     * @param InputInterface $input Symfony's Input Class for parameters and options
173 28
     * @param OutputInterface $output Symfony's Output Class to display info
174 28
     *
175 28
     * @return int|null null or 0 if everything went fine, or an error code
176 28
     *
177 28
     * @throws \Doctrine\DBAL\DBALException
178 28
     * @throws \Edyan\Neuralyzer\Exception\NeuralyzerException
179
     */
180
    protected function execute(InputInterface $input, OutputInterface $output): int
181 28
    {
182
        // Throw an exception immediately if we don't have the required DB parameter
183
        if (empty($input->getOption('db'))) {
184
            throw new \InvalidArgumentException('Database name is required (--db)');
185
        }
186
187
        // Throw an exception immediately if we don't have the right mode
188
        if (! in_array($input->getOption('mode'), ['queries', 'batch'])) {
189
            throw new \InvalidArgumentException('--mode could be only "queries" or "batch"');
190
        }
191 18
192
        $password = $input->getOption('password');
193 18
        if ($password === null) {
194
            $question = new Question('Password: ');
195
            $question->setHidden(true)->setHiddenFallback(false);
196
197
            $password = $this->getHelper('question')->ask($input, $output, $question);
198 18
        }
199 1
200
        $this->input = $input;
201
        $this->output = $output;
202
203 17
        // Anon READER
204 1
        $this->reader = new Reader($input->getOption('config'));
205
        if (! empty($this->reader->getDepreciationMessages())) {
206
            foreach ($this->reader->getDepreciationMessages() as $message) {
207 16
                $output->writeLn("<comment>WARNING : ${message}</comment>");
208 16
            }
209 4
        }
210 4
211
        $this->dbUtils->configure([
212 4
            'driver' => $input->getOption('driver'),
213
            'host' => $input->getOption('host'),
214
            'dbname' => $input->getOption('db'),
215 16
            'user' => $input->getOption('user'),
216 16
            'password' => $password,
217
        ]);
218
219 16
        $this->db = new DB($this->expression, $this->dbUtils);
220 16
        $this->db->setConfiguration($this->reader);
221
        $this->db->setMode($this->input->getOption('mode'));
222
        $this->db->setPretend($this->input->getOption('pretend'));
223
        $this->db->setReturnRes($this->input->getOption('sql'));
224
225
        if (! empty($input->getOption('bootstrap'))) {
226 16
            FileLoader::checkAndLoad($input->getOption('bootstrap'));
227 16
        }
228 16
229 16
        $stopwatch = new Stopwatch();
230 16
        $stopwatch->start('Neuralyzer');
231 16
        // Get tables
232
        $table = $input->getOption('table');
233
        $tables = empty($table) ? $this->reader->getEntities() : [$table];
234 15
        $hasErrors = false;
235 15
        foreach ($tables as $table) {
236 15
            if (! $this->anonymizeTable($table)) {
237 15
                $hasErrors = true;
238 15
            }
239
        }
240 15
241 15
        // Get memory and execution time information
242
        $event = $stopwatch->stop('Neuralyzer');
243 15
        $memory = round($event->getMemory() / 1024 / 1024, 2);
244 15
        $time = round($event->getDuration() / 1000, 2);
245 15
        $time = ($time > 180 ? round($time / 60, 2).'min' : "${time} sec");
246 15
247 15
        $output->writeln("<info>Done in ${time} using ${memory} Mb of memory</info>");
248 13
249
        return $hasErrors ? Command::FAILURE : Command::SUCCESS;
250
    }
251
252
    /**
253 13
     * Anonymize a specific table and display info about the job
254 13
     */
255 13
    private function anonymizeTable(string $table): bool
256 13
    {
257
        $total = $this->getTotal($table);
258 13
        if ($total === 0) {
259
            $this->output->writeln("<info>${table} is empty</info>");
260 13
261
            return true;
262
        }
263
264
        $bar = new ProgressBar($this->output, $total);
265
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
266
267
        $this->output->writeln("<info>Anonymizing ${table}</info>");
268
269
        try {
270 15
            $queries = $this->db->setLimit($total)->processEntity(
271
                $table,
272 15
                static function () use ($bar): void {
273 13
                    $bar->advance();
274 2
                }
275
            );
276 2
            // @codeCoverageIgnoreStart
277
        } catch (\Exception $e) {
278
            $msg = "<error>Error anonymizing ${table}. Message was : " . $e->getMessage() . '</error>';
279 11
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
280 11
281
            return false;
282 11
        }
283
        // @codeCoverageIgnoreEnd
284
285 11
        $this->output->writeln(PHP_EOL);
286 11
287
        if ($this->input->getOption('sql')) {
288 10
            $this->output->writeln('<comment>Queries:</comment>');
289 11
            $this->output->writeln(implode(PHP_EOL, $queries));
290
            $this->output->writeln(PHP_EOL);
291
        }
292
293
        return true;
294
    }
295
296
    /**
297
     * Define the total number of records to process for progress bar
298
     */
299
    private function getTotal(string $table): int
300 10
    {
301
        $config = $this->reader->getEntityConfig($table);
302 10
        $limit = (int) $config['limit'];
303 2
        if ($config['action'] === 'insert') {
304 2
            return empty($limit) ? 100 : $limit;
305 2
        }
306
307
        $rows = $this->dbUtils->countResults($table);
308 10
        if (empty($limit)) {
309
            return $rows;
310
        }
311
312
        if (! empty($limit) && $limit > $rows) {
313
            return $rows;
314
        }
315
316
        return $limit;
317
    }
318
}
319