Passed
Push — master ( 1ab114...d8759b )
by Emmanuel
02:02
created

RunCommand::getTotal()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 8
cts 8
cp 1
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 11
nc 5
nop 1
crap 6
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 17
    protected function configure(): void
76
    {
77
        // First command : Test the DB Connexion
78 17
        $this->setName($this->command)
79 17
            ->setDescription('Run Anonymizer')
80 17
            ->setHelp(
81 17
                'This command will connect to a DB and run the anonymizer from a yaml config' . PHP_EOL .
82 17
                "Usage: <info>./bin/neuralyzer {$this->command} -u app -p app -f neuralyzer.yml</info>"
83 17
            )->addOption(
84 17
                'driver',
85 17
                'D',
86 17
                InputOption::VALUE_REQUIRED,
87 17
                'Driver (check Doctrine documentation to have the list)',
88 17
                'pdo_mysql'
89 17
            )->addOption(
90 17
                'host',
91 17
                'H',
92 17
                InputOption::VALUE_REQUIRED,
93 17
                'Host',
94 17
                '127.0.0.1'
95 17
            )->addOption(
96 17
                'db',
97 17
                'd',
98 17
                InputOption::VALUE_REQUIRED,
99 17
                'Database Name'
100 17
            )->addOption(
101 17
                'user',
102 17
                'u',
103 17
                InputOption::VALUE_REQUIRED,
104 17
                'User Name',
105 17
                get_current_user()
106 17
            )->addOption(
107 17
                'password',
108 17
                'p',
109 17
                InputOption::VALUE_REQUIRED,
110 17
                'Password (or prompted)'
111 17
            )->addOption(
112 17
                'config',
113 17
                'c',
114 17
                InputOption::VALUE_REQUIRED,
115 17
                'Configuration File',
116 17
                'neuralyzer.yml'
117 17
            )->addOption(
118 17
                'table',
119 17
                't',
120 17
                InputOption::VALUE_REQUIRED,
121 17
                'Do a single table'
122 17
            )->addOption(
123 17
                'pretend',
124 17
                null,
125 17
                InputOption::VALUE_NONE,
126 17
                "Don't run the queries"
127 17
            )->addOption(
128 17
                'sql',
129 17
                's',
130 17
                InputOption::VALUE_NONE,
131 17
                'Display the SQL'
132 17
            )->addOption(
133 17
                'limit',
134 17
                'l',
135 17
                InputOption::VALUE_REQUIRED,
136 17
                'Limit the number of written records (update or insert). 100 by default for insert'
137
            );
138 17
    }
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
        try {
216
            $queries = $this->db->setLimit($total)->processEntity($table, function () use ($bar) {
217 2
                $bar->advance();
218 2
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
219 2
        // @codeCoverageIgnoreStart
220
        } catch (\Exception $e) {
221
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
222
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
223
            return;
224
        }
225
        // @codeCoverageIgnoreEnd
226
227
        $this->output->writeln(PHP_EOL);
228 2
229
        if ($this->input->getOption('sql')) {
230 2
            $this->output->writeln('<comment>Queries:</comment>');
231 1
            $this->output->writeln(implode(PHP_EOL, $queries));
232 1
            $this->output->writeln(PHP_EOL);
233 1
        }
234
    }
235 2
236
237
    /**
238
     * Define the total number of records to process for progress bar
239
     *
240
     * @param  string $table
241
     * @return int
242
     */
243
    private function getTotal(string $table): int
244 5
    {
245
        $limit = (int)$this->input->getOption('limit');
246
        $config = $this->reader->getEntityConfig($table);
247 5
        if ($config['action'] === 'insert') {
248 4
            return empty($limit) ? 100 : $limit;
249 2
        }
250 2
251 2
        $rows = $this->db->countResults($table);
252
        if (empty($limit)) {
253
            return $rows;
254 3
        }
255
256 3
        if (!empty($limit) && $limit > $rows) {
257
            return $rows;
258
        }
259
260
        return $limit;
261
    }
262
}
263