Completed
Push — master ( 759e71...716894 )
by Emmanuel
05:49
created

RunCommand::countRecords()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 1
1
<?php
2
/**
3
 * neuralyzer : Data Anonymization Library and CLI Tool
4
 *
5
 * PHP Version 7.0
6
 *
7
 * @author Emmanuel Dyan
8
 * @author Rémi Sauvat
9
 * @copyright 2017 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 Inet\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
    use DBTrait;
34
35
    /**
36
     * Set the command shortcut to be used in configuration
37
     *
38
     * @var string
39
     */
40
    private $command = 'run';
41
42
    /**
43
     * @var InputInterface
44
     */
45
    private $input;
46
47
    /**
48
     * @var OutputInterface
49
     */
50
    private $output;
51
52
53
    /**
54
     * Configure the command
55
     *
56
     * @return void
57
     */
58
    protected function configure()
59
    {
60
        // First command : Test the DB Connexion
61
        $this->setName($this->command)
62
            ->setDescription('Generate configuration for the Anonymizer')
63
            ->setHelp(
64
                'This command will connect to a DB and run the anonymizer from a yaml config' . PHP_EOL .
65
                "Usage: ./bin/neuralyzer <info>{$this->command} -u app -p app -f anon.yml</info>"
66
            )->addOption(
67
                'host',
68
                null,
69
                InputOption::VALUE_REQUIRED,
70
                'Host',
71
                '127.0.0.1'
72
            )->addOption(
73
                'db',
74
                'd',
75
                InputOption::VALUE_REQUIRED,
76
                'Database Name'
77
            )->addOption(
78
                'user',
79
                'u',
80
                InputOption::VALUE_REQUIRED,
81
                'User Name',
82
                get_current_user()
83
            )->addOption(
84
                'password',
85
                'p',
86
                InputOption::VALUE_REQUIRED,
87
                'Password (or prompted)'
88
            )->addOption(
89
                'config',
90
                'c',
91
                InputOption::VALUE_REQUIRED,
92
                'Configuration File',
93
                'anon.yml'
94
            )->addOption(
95
                'pretend',
96
                null,
97
                InputOption::VALUE_NONE,
98
                "Don't run the queries"
99
            )->addOption(
100
                'sql',
101
                null,
102
                InputOption::VALUE_NONE,
103
                'Display the SQL'
104
            );
105
    }
106
107
    /**
108
     * Execute the command
109
     *
110
     * @param InputInterface  $input
111
     * @param OutputInterface $output
112
     *
113
     * @return void
114
     */
115
    protected function execute(InputInterface $input, OutputInterface $output)
116
    {
117
        $password = $input->getOption('password');
118
        if (is_null($password)) {
119
            $question = new Question('Password: ');
120
            $question->setHidden(true)->setHiddenFallback(false);
121
122
            $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...
123
        }
124
125
        $this->connectToDB(
126
            $input->getOption('host'),
127
            $input->getOption('db'),
128
            $input->getOption('user'),
129
            $password
130
        );
131
132
        $this->input = $input;
133
        $this->output = $output;
134
135
        // Anon READER
136
        $reader = new \Inet\Neuralyzer\Configuration\Reader($input->getOption('config'));
137
138
        // Now work on the DB
139
        $this->anon = new \Inet\Neuralyzer\Anonymizer\DB($this->pdo);
0 ignored issues
show
Bug introduced by
The property anon does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
140
        $this->anon->setConfiguration($reader);
141
142
        $stopwatch = new Stopwatch();
143
        $stopwatch->start('Neuralyzer');
144
        // Get tables
145
        $tables = $reader->getEntities();
146
        foreach ($tables as $table) {
147
            $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...
148
        }
149
150
        // Get memory and execution time information
151
        $event = $stopwatch->stop('Neuralyzer');
152
        $memory = round($event->getMemory() / 1024 / 1024, 2);
153
        $time = round($event->getDuration() / 1000, 2);
154
        $time = ($time > 180 ? round($time / 60, 2) . 'mins' : "$time sec");
155
156
        $output->writeln("<info>Done in $time using $memory Mb of memory</info>");
157
    }
158
159
    /**
160
     * Anonmyze a specific table and display info about the job
161
     *
162
     * @param  string $table
163
     */
164
    private function anonymizeTable(string $table)
165
    {
166
        $total = $this->countRecords($table);
167
        if ($total === 0) {
168
            $this->output->writeln("<info>$table is empty</info>");
169
            return;
170
        }
171
172
        $bar = new ProgressBar($this->output, $total);
173
        $bar->setRedrawFrequency($total > 100 ? 100 : 0);
174
175
        $this->output->writeln("<info>Anonymizing $table</info>");
176
177
        try {
178
            $queries = $this->anon->processEntity($table, function () use ($bar) {
179
                $bar->advance();
180
            }, $this->input->getOption('pretend'), $this->input->getOption('sql'));
181
        } catch (\Exception $e) {
182
            $msg = "<error>Error anonymizing $table. Message was : " . $e->getMessage() . "</error>";
183
            $this->output->writeln(PHP_EOL . $msg . PHP_EOL);
184
            return;
185
        }
186
187
        $this->output->writeln(PHP_EOL);
188
189
        if ($this->input->getOption('sql')) {
190
            $this->output->writeln('<comment>Queries:</comment>');
191
            $this->output->writeln(implode(PHP_EOL, $queries));
192
            $this->output->writeln(PHP_EOL);
193
        }
194
    }
195
196
    /**
197
     * Count records on a table
198
     * @param  string $table
199
     * @return int
200
     */
201
    private function countRecords(string $table): int
202
    {
203
        try {
204
            $result = $this->pdo->query("SELECT COUNT(1) FROM $table");
205
        } catch (\Exception $e) {
206
            $msg = "Could not count records in table '$table' defined in your config";
207
            throw new \InvalidArgumentException($msg);
208
        }
209
210
        $data = $result->fetchAll(\PDO::FETCH_COLUMN);
211
212
        return (int)$data[0];
213
    }
214
}
215