Completed
Pull Request — master (#9)
by Emmanuel
02:52
created

RunCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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