Passed
Push — master ( c10078...a55606 )
by Emmanuel
01:54
created

RunCommand::anonymizeTable()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

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

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
172
        }
173
174
        // Get memory and execution time information
175 3
        $event = $stopwatch->stop('Neuralyzer');
176 3
        $memory = round($event->getMemory() / 1024 / 1024, 2);
177 3
        $time = round($event->getDuration() / 1000, 2);
178 3
        $time = ($time > 180 ? round($time / 60, 2) . 'mins' : "$time sec");
179
180 3
        $output->writeln("<info>Done in $time using $memory Mb of memory</info>");
181 3
    }
182
183
    /**
184
     * Anonmyze a specific table and display info about the job
185
     *
186
     * @param  string $table
187
     */
188 5
    private function anonymizeTable(string $table)
189
    {
190 5
        $total = $this->countRecords($table);
191 3
        if ($total === 0) {
192 1
            $this->output->writeln("<info>$table is empty</info>");
193 1
            return;
194
        }
195
196 2
        $bar = new ProgressBar($this->output, $total);
197 2
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
198
199 2
        $this->output->writeln("<info>Anonymizing $table</info>");
200
201
        try {
202 2
            $queries = $this->db->processEntity($table, function () use ($bar) {
203 2
                $bar->advance();
204 2
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
205
        // @codeCoverageIgnoreStart
206
        } catch (\Exception $e) {
207
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
208
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
209
            return;
210
        }
211
        // @codeCoverageIgnoreEnd
212
213 2
        $this->output->writeln(PHP_EOL);
214
215 2
        if ($this->input->getOption('sql')) {
216 1
            $this->output->writeln('<comment>Queries:</comment>');
217 1
            $this->output->writeln(implode(PHP_EOL, $queries));
218 1
            $this->output->writeln(PHP_EOL);
219
        }
220 2
    }
221
222
    /**
223
     * Count records on a table
224
     * @param  string $table
225
     * @return int
226
     */
227 5
    private function countRecords(string $table): int
228
    {
229
        try {
230 5
            $stmt = $this->db->getConn()->prepare("SELECT COUNT(1) AS total FROM $table");
231 4
            $stmt->execute();
232 2
        } catch (\Exception $e) {
233 2
            $msg = "Could not count records in '$table' from your config : " . $e->getMessage();
234 2
            throw new \InvalidArgumentException($msg);
235
        }
236
237 3
        $data = $stmt->fetchAll();
238
239 3
        return (int)$data[0]['total'];
240
    }
241
}
242