Passed
Push — master ( bf62c3...8c031b )
by Emmanuel
01:56
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 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
    protected function configure()
68
    {
69
        // First command : Test the DB Connexion
70
        $this->setName($this->command)
71
            ->setDescription('Generate configuration for the Anonymizer')
72
            ->setHelp(
73
                'This command will connect to a DB and run the anonymizer from a yaml config' . PHP_EOL .
74
                "Usage: ./bin/neuralyzer <info>{$this->command} -u app -p app -f neuralyzer.yml</info>"
75 13
            )->addOption(
76
                'driver',
77
                '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 13
            );
125 13
    }
126 13
127 13
    /**
128 13
     * Execute the command
129 13
     *
130 13
     * @param InputInterface  $input   Symfony's Input Class for parameters and options
131 13
     * @param OutputInterface $output  Symfony's Output Class to display infos
132 13
     *
133 13
     * @return void
134 13
     */
135 13
    protected function execute(InputInterface $input, OutputInterface $output)
136 13
    {
137
        // Throw an exception immediately if we dont have the required DB parameter
138 13
        if (empty($input->getOption('db'))) {
139
            throw new \InvalidArgumentException('Database name is required (--db)');
140
        }
141
142
        $password = $input->getOption('password');
143
        if (is_null($password)) {
144
            $question = new Question('Password: ');
145
            $question->setHidden(true)->setHiddenFallback(false);
146
147
            $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 7
        }
149
150
        $this->input = $input;
151 7
        $this->output = $output;
152 1
153
        // Anon READER
154
        $reader = new \Edyan\Neuralyzer\Configuration\Reader($input->getOption('config'));
155 6
156 6
        // Now work on the DB
157 2
        $this->db = new \Edyan\Neuralyzer\Anonymizer\DB([
158 2
            'driver' => $input->getOption('driver'),
159
            'host' => $input->getOption('host'),
160 2
            'dbname' => $input->getOption('db'),
161
            'user' => $input->getOption('user'),
162
            'password' => $password,
163 6
        ]);
164 6
        $this->db->setConfiguration($reader);
165
166
        $stopwatch = new Stopwatch();
167 6
        $stopwatch->start('Neuralyzer');
168
        // Get tables
169
        $tables = empty($input->getOption('table')) ? $reader->getEntities() : [$input->getOption('table')];
170 6
        foreach ($tables as $table) {
171 6
            $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 6
        }
173 6
174 6
        // Get memory and execution time information
175 6
        $event = $stopwatch->stop('Neuralyzer');
176
        $memory = round($event->getMemory() / 1024 / 1024, 2);
177 5
        $time = round($event->getDuration() / 1000, 2);
178
        $time = ($time > 180 ? round($time / 60, 2) . 'mins' : "$time sec");
179 5
180 5
        $output->writeln("<info>Done in $time using $memory Mb of memory</info>");
181
    }
182 5
183 5
    /**
184 5
     * Anonmyze a specific table and display info about the job
185 5
     *
186
     * @param  string $table
187
     */
188
    private function anonymizeTable(string $table)
189 3
    {
190 3
        $total = $this->countRecords($table);
191 3
        if ($total === 0) {
192 3
            $this->output->writeln("<info>$table is empty</info>");
193
            return;
194 3
        }
195 3
196
        $bar = new ProgressBar($this->output, $total);
197
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
198
199
        $this->output->writeln("<info>Anonymizing $table</info>");
200
201
        try {
202 5
            $queries = $this->db->processEntity($table, function () use ($bar) {
203
                $bar->advance();
204 5
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
205 3
        // @codeCoverageIgnoreStart
206 1
        } catch (\Exception $e) {
207 1
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
208
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
209
            return;
210 2
        }
211 2
        // @codeCoverageIgnoreEnd
212
213 2
        $this->output->writeln(PHP_EOL);
214
215
        if ($this->input->getOption('sql')) {
216 2
            $this->output->writeln('<comment>Queries:</comment>');
217 2
            $this->output->writeln(implode(PHP_EOL, $queries));
218 2
            $this->output->writeln(PHP_EOL);
219
        }
220
    }
221
222
    /**
223
     * Count records on a table
224
     * @param  string $table
225
     * @return int
226
     */
227 2
    private function countRecords(string $table): int
228
    {
229 2
        try {
230 1
            $stmt = $this->db->getConn()->prepare("SELECT COUNT(1) AS total FROM $table");
231 1
            $stmt->execute();
232 1
        } catch (\Exception $e) {
233
            $msg = "Could not count records in '$table' from your config : " . $e->getMessage();
234 2
            throw new \InvalidArgumentException($msg);
235
        }
236
237
        $data = $stmt->fetchAll();
238
239
        return (int)$data[0]['total'];
240
    }
241
}
242