RunCommand::execute()   C
last analyzed

Complexity

Conditions 12
Paths 194

Size

Total Lines 70
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 12.0707

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 42
c 2
b 0
f 0
nc 194
nop 2
dl 0
loc 70
ccs 35
cts 38
cp 0.9211
crap 12.0707
rs 6.1833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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