Completed
Push — master ( f0d93b...12af13 )
by Emmanuel
04:54
created

RunCommand::countRecords()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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