Passed
Push — master ( c103e2...560387 )
by Emmanuel
03:39
created

RunCommand::execute()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 29
cts 29
cp 1
rs 8.5125
c 0
b 0
f 0
nc 9
cc 5
eloc 28
nop 2
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
     * @var InputInterface
49
     */
50
    private $input;
51
52
    /**
53
     * @var OutputInterface
54
     */
55
    private $output;
56
57
58
    /**
59
     * Configure the command
60
     *
61
     * @return void
62
     */
63 13
    protected function configure()
64
    {
65
        // First command : Test the DB Connexion
66 13
        $this->setName($this->command)
67 13
            ->setDescription('Generate configuration for the Anonymizer')
68 13
            ->setHelp(
69 13
                'This command will connect to a DB and run the anonymizer from a yaml config' . PHP_EOL .
70 13
                "Usage: ./bin/neuralyzer <info>{$this->command} -u app -p app -f anon.yml</info>"
71 13
            )->addOption(
72 13
                'driver',
73 13
                null,
74 13
                InputOption::VALUE_REQUIRED,
75 13
                'Driver (check Doctrine documentation to have the list)',
76 13
                'pdo_mysql'
77 13
            )->addOption(
78 13
                'host',
79 13
                null,
80 13
                InputOption::VALUE_REQUIRED,
81 13
                'Host',
82 13
                '127.0.0.1'
83 13
            )->addOption(
84 13
                'db',
85 13
                'd',
86 13
                InputOption::VALUE_REQUIRED,
87 13
                'Database Name'
88 13
            )->addOption(
89 13
                'user',
90 13
                'u',
91 13
                InputOption::VALUE_REQUIRED,
92 13
                'User Name',
93 13
                get_current_user()
94 13
            )->addOption(
95 13
                'password',
96 13
                'p',
97 13
                InputOption::VALUE_REQUIRED,
98 13
                'Password (or prompted)'
99 13
            )->addOption(
100 13
                'config',
101 13
                'c',
102 13
                InputOption::VALUE_REQUIRED,
103 13
                'Configuration File',
104 13
                'anon.yml'
105 13
            )->addOption(
106 13
                'pretend',
107 13
                null,
108 13
                InputOption::VALUE_NONE,
109 13
                "Don't run the queries"
110 13
            )->addOption(
111 13
                'sql',
112 13
                null,
113 13
                InputOption::VALUE_NONE,
114 13
                'Display the SQL'
115
            );
116 13
    }
117
118
    /**
119
     * Execute the command
120
     *
121
     * @param InputInterface  $input
122
     * @param OutputInterface $output
123
     *
124
     * @return void
125
     */
126 7
    protected function execute(InputInterface $input, OutputInterface $output)
127
    {
128
        // Throw an exception immediately if we dont have the required DB parameter
129 7
        if (empty($input->getOption('db'))) {
130 1
            throw new \InvalidArgumentException('Database name is required (--db)');
131
        }
132
133 6
        $password = $input->getOption('password');
134 6
        if (is_null($password)) {
135 2
            $question = new Question('Password: ');
136 2
            $question->setHidden(true)->setHiddenFallback(false);
137
138 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...
139
        }
140
141 6
        $this->input = $input;
142 6
        $this->output = $output;
143
144
        // Anon READER
145 6
        $reader = new \Edyan\Neuralyzer\Configuration\Reader($input->getOption('config'));
146
147
        // Now work on the DB
148 6
        $this->db = new \Edyan\Neuralyzer\Anonymizer\DB([
149 6
            'driver' => $input->getOption('driver'),
150 6
            'host' => $input->getOption('host'),
151 6
            'dbname' => $input->getOption('db'),
152 6
            'user' => $input->getOption('user'),
153 6
            'password' => $password,
154
        ]);
155 5
        $this->db->setConfiguration($reader);
156
157 5
        $stopwatch = new Stopwatch();
158 5
        $stopwatch->start('Neuralyzer');
159
        // Get tables
160 5
        $tables = $reader->getEntities();
161 5
        foreach ($tables as $table) {
162 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...
163
        }
164
165
        // Get memory and execution time information
166 3
        $event = $stopwatch->stop('Neuralyzer');
167 3
        $memory = round($event->getMemory() / 1024 / 1024, 2);
168 3
        $time = round($event->getDuration() / 1000, 2);
169 3
        $time = ($time > 180 ? round($time / 60, 2) . 'mins' : "$time sec");
170
171 3
        $output->writeln("<info>Done in $time using $memory Mb of memory</info>");
172 3
    }
173
174
    /**
175
     * Anonmyze a specific table and display info about the job
176
     *
177
     * @param  string $table
178
     */
179 5
    private function anonymizeTable(string $table)
180
    {
181 5
        $total = $this->countRecords($table);
182 3
        if ($total === 0) {
183 1
            $this->output->writeln("<info>$table is empty</info>");
184 1
            return;
185
        }
186
187 2
        $bar = new ProgressBar($this->output, $total);
188 2
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
189
190 2
        $this->output->writeln("<info>Anonymizing $table</info>");
191
192
        try {
193 2
            $queries = $this->db->processEntity($table, function () use ($bar) {
194 2
                $bar->advance();
195 2
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
196
        } catch (\Exception $e) {
197
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
198
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
199
            return;
200
        }
201
202 2
        $this->output->writeln(PHP_EOL);
203
204 2
        if ($this->input->getOption('sql')) {
205 1
            $this->output->writeln('<comment>Queries:</comment>');
206 1
            $this->output->writeln(implode(PHP_EOL, $queries));
207 1
            $this->output->writeln(PHP_EOL);
208
        }
209 2
    }
210
211
    /**
212
     * Count records on a table
213
     * @param  string $table
214
     * @return int
215
     */
216 5
    private function countRecords(string $table): int
217
    {
218
        try {
219 5
            $stmt = $this->db->getConn()->prepare("SELECT COUNT(1) AS total FROM $table");
220 4
            $stmt->execute();
221 2
        } catch (\Exception $e) {
222 2
            $msg = "Could not count records in '$table' from your config : " . $e->getMessage();
223 2
            throw new \InvalidArgumentException($msg);
224
        }
225
226 3
        $data = $stmt->fetchAll();
227
228 3
        return (int)$data[0]['total'];
229
    }
230
}
231