Completed
Push — master ( 275c1f...cb75c2 )
by Emmanuel
05:43
created

RunCommand::getTotal()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 7.7305

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
ccs 7
cts 11
cp 0.6364
cc 6
eloc 11
nc 5
nop 1
crap 7.7305
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\Configuration\Reader;
21
use Symfony\Component\Console\Command\Command;
22
use Symfony\Component\Console\Helper\ProgressBar;
23
use Symfony\Component\Console\Input\InputInterface;
24
use Symfony\Component\Console\Input\InputOption;
25
use Symfony\Component\Console\Output\OutputInterface;
26
use Symfony\Component\Console\Question\Question;
27
use Symfony\Component\Stopwatch\Stopwatch;
28
29
/**
30
 * Command to launch an anonymization based on a config file
31
 */
32
class RunCommand extends Command
33
{
34
    /**
35
     * Store the DB Object
36
     *
37
     * @var \Edyan\Neuralyzer\Anonymizer\DB
38
     */
39
    private $db;
40
41
    /**
42
     * Set the command shortcut to be used in configuration
43
     *
44
     * @var string
45
     */
46
    private $command = 'run';
47
48
    /**
49
     * Symfony's Input Class for parameters and options
50
     *
51
     * @var InputInterface
52
     */
53
    private $input;
54
55
    /**
56
     * Symfony's Output Class to display info
57
     *
58
     * @var OutputInterface
59
     */
60
    private $output;
61
62
    /**
63
     * Neuralyzer reader
64
     *
65
     * @var Reader
66
     */
67
    private $reader;
68
69
70
    /**
71
     * Configure the command
72
     *
73
     * @return void
74
     */
75 15
    protected function configure(): void
76
    {
77
        // First command : Test the DB Connexion
78 15
        $this->setName($this->command)
79 15
            ->setDescription('Run Anonymizer')
80 15
            ->setHelp(
81 15
                'This command will connect to a DB and run the anonymizer from a yaml config' . PHP_EOL .
82 15
                "Usage: <info>./bin/neuralyzer {$this->command} -u app -p app -f neuralyzer.yml</info>"
83 15
            )->addOption(
84 15
                'driver',
85 15
                'D',
86 15
                InputOption::VALUE_REQUIRED,
87 15
                'Driver (check Doctrine documentation to have the list)',
88 15
                'pdo_mysql'
89 15
            )->addOption(
90 15
                'host',
91 15
                'H',
92 15
                InputOption::VALUE_REQUIRED,
93 15
                'Host',
94 15
                '127.0.0.1'
95 15
            )->addOption(
96 15
                'db',
97 15
                'd',
98 15
                InputOption::VALUE_REQUIRED,
99 15
                'Database Name'
100 15
            )->addOption(
101 15
                'user',
102 15
                'u',
103 15
                InputOption::VALUE_REQUIRED,
104 15
                'User Name',
105 15
                get_current_user()
106 15
            )->addOption(
107 15
                'password',
108 15
                'p',
109 15
                InputOption::VALUE_REQUIRED,
110 15
                'Password (or prompted)'
111 15
            )->addOption(
112 15
                'config',
113 15
                'c',
114 15
                InputOption::VALUE_REQUIRED,
115 15
                'Configuration File',
116 15
                'neuralyzer.yml'
117 15
            )->addOption(
118 15
                'table',
119 15
                't',
120 15
                InputOption::VALUE_REQUIRED,
121 15
                'Do a single table'
122 15
            )->addOption(
123 15
                'pretend',
124 15
                null,
125 15
                InputOption::VALUE_NONE,
126 15
                "Don't run the queries"
127 15
            )->addOption(
128 15
                'sql',
129 15
                's',
130 15
                InputOption::VALUE_NONE,
131 15
                'Display the SQL'
132 15
            )->addOption(
133 15
                'limit',
134 15
                'l',
135 15
                InputOption::VALUE_REQUIRED,
136 15
                'Limit the number of written records (update or insert). 100 by default for insert'
137
            );
138 15
    }
139
140
    /**
141
     * Execute the command
142
     *
143
     * @param InputInterface  $input   Symfony's Input Class for parameters and options
144
     * @param OutputInterface $output  Symfony's Output Class to display infos
145
     *
146
     * @return void
147
     */
148 7
    protected function execute(InputInterface $input, OutputInterface $output): void
149
    {
150
        // Throw an exception immediately if we dont have the required DB parameter
151 7
        if (empty($input->getOption('db'))) {
152 1
            throw new \InvalidArgumentException('Database name is required (--db)');
153
        }
154
155 6
        $password = $input->getOption('password');
156 6
        if (is_null($password)) {
157 2
            $question = new Question('Password: ');
158 2
            $question->setHidden(true)->setHiddenFallback(false);
159
160 2
            $password = $this->getHelper('question')->ask($input, $output, $question);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Helper\HelperInterface as the method ask() does only exist in the following implementations of said interface: Symfony\Component\Console\Helper\QuestionHelper, Symfony\Component\Consol...r\SymfonyQuestionHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
161
        }
162
163 6
        $this->input = $input;
164 6
        $this->output = $output;
165
166
        // Anon READER
167 6
        $this->reader = new Reader($input->getOption('config'));
168
169
        // Now work on the DB
170 6
        $this->db = new \Edyan\Neuralyzer\Anonymizer\DB([
171 6
            'driver' => $input->getOption('driver'),
172 6
            'host' => $input->getOption('host'),
173 6
            'dbname' => $input->getOption('db'),
174 6
            'user' => $input->getOption('user'),
175 6
            'password' => $password,
176
        ]);
177 5
        $this->db->setConfiguration($this->reader);
178
179 5
        $stopwatch = new Stopwatch();
180 5
        $stopwatch->start('Neuralyzer');
181
        // Get tables
182 5
        $table = $input->getOption('table');
183 5
        $tables = empty($table) ? $this->reader->getEntities() : [$table];
184 5
        foreach ($tables as $table) {
185 5
            $this->anonymizeTable($table);
186
        }
187
188
        // Get memory and execution time information
189 3
        $event = $stopwatch->stop('Neuralyzer');
190 3
        $memory = round($event->getMemory() / 1024 / 1024, 2);
191 3
        $time = round($event->getDuration() / 1000, 2);
192 3
        $time = ($time > 180 ? round($time / 60, 2) . 'mins' : "$time sec");
193
194 3
        $output->writeln("<info>Done in $time using $memory Mb of memory</info>");
195 3
    }
196
197
    /**
198
     * Anonmyze a specific table and display info about the job
199
     *
200
     * @param  string $table
201
     */
202 5
    private function anonymizeTable(string $table): void
203
    {
204 5
        $total = $this->getTotal($table);
205 3
        if ($total === 0) {
206 1
            $this->output->writeln("<info>$table is empty</info>");
207 1
            return;
208
        }
209
210 2
        $bar = new ProgressBar($this->output, $total);
211 2
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
212
213 2
        $this->output->writeln("<info>Anonymizing $table</info>");
214
215 2
        $this->db->setLimit($total);
216
        try {
217 2
            $queries = $this->db->processEntity($table, function () use ($bar) {
218 2
                $bar->advance();
219 2
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
220
        // @codeCoverageIgnoreStart
221
        } catch (\Exception $e) {
222
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
223
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
224
            return;
225
        }
226
        // @codeCoverageIgnoreEnd
227
228 2
        $this->output->writeln(PHP_EOL);
229
230 2
        if ($this->input->getOption('sql')) {
231 1
            $this->output->writeln('<comment>Queries:</comment>');
232 1
            $this->output->writeln(implode(PHP_EOL, $queries));
233 1
            $this->output->writeln(PHP_EOL);
234
        }
235 2
    }
236
237
238
    /**
239
     * Count records on a table
240
     *
241
     * @param  string $table
242
     * @return int
243
     */
244 5
    private function countRecords(string $table): int
245
    {
246
        try {
247 5
            $stmt = $this->db->getConn()->prepare("SELECT COUNT(1) AS total FROM $table");
248 4
            $stmt->execute();
249 2
        } catch (\Exception $e) {
250 2
            $msg = "Could not count records in '$table' from your config : " . $e->getMessage();
251 2
            throw new \InvalidArgumentException($msg);
252
        }
253
254 3
        $data = $stmt->fetchAll();
255
256 3
        return (int)$data[0]['total'];
257
    }
258
259
260
    /**
261
     * Define the total number of records to process for progress bar
262
     *
263
     * @param  string $table
264
     * @return int
265
     */
266 5
    private function getTotal(string $table): int
267
    {
268 5
        $limit = $this->input->getOption('limit');
269 5
        $config = $this->reader->getEntityConfig($table);
270 5
        if ($config['action'] === 'insert') {
271
            return empty($limit) ? 100 : $limit;
272
        }
273
274 5
        $rows = $this->countRecords($table);
275 3
        if (empty($limit)) {
276 3
            return $rows;
277
        }
278
279
        if (!empty($limit) && $limit > $rows) {
280
            return $rows;
281
        }
282
283
        return $limit;
284
    }
285
}
286