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

DB::prepareUpdate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
ccs 10
cts 10
cp 1
cc 2
eloc 10
nc 2
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\Anonymizer;
19
20
use Doctrine\DBAL\Configuration as DbalConfiguration;
21
use Doctrine\DBAL\DriverManager as DbalDriverManager;
22
use Doctrine\DBAL\Query\QueryBuilder;
23
use Edyan\Neuralyzer\Exception\NeuralizerException;
24
25
/**
26
 * Implement AbstractAnonymizer for DB, to read and write data via Doctrine DBAL
27
 */
28
class DB extends AbstractAnonymizer
29
{
30
    /**
31
     * Doctrine DB Adapter
32
     *
33
     * @var \Doctrine\DBAL\Connection
34
     */
35
    private $conn;
36
37
38
    /**
39
     * Primary Key
40
     * @var string
41
     */
42
    private $priKey;
43
44
45
    /**
46
     * Init connection
47
     *
48
     * @param $params   Parameters to send to Doctrine DB
49
     */
50 31
    public function __construct(array $params)
51
    {
52 31
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
53 30
    }
54
55
56
    /**
57
     * Get Doctrine Connection
58
     * @return Doctrine\DBAL\Connection
59
     */
60 22
    public function getConn()
61
    {
62 22
        return $this->conn;
63
    }
64
65
66
    /**
67
     * Process an entity by reading / writing to the DB
68
     *
69
     * @param string        $entity
70
     * @param callable|null $callback
71
     * @param bool          $pretend
72
     * @param bool          $returnRes
73
     *
74
     * @return void|array
75
     */
76 15
    public function processEntity(
77
        string $entity,
78
        callable $callback = null,
79
        bool $pretend = true,
80
        bool $returnRes = false,
81
        int $limit = 0
82
    ): array {
83 15
        $schema = $this->conn->getSchemaManager();
84 15
        if ($schema->tablesExist($entity) === false) {
0 ignored issues
show
Documentation introduced by
$entity is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
85 1
            throw new NeuralizerException("Table $entity does not exist");
86
        }
87
88 14
        $this->entity = $entity;
89 14
        $this->priKey = $this->getPrimaryKey();
90 13
        $this->entityCols = $this->getTableCols();
91
92 13
        $queries = [];
93
94 13
        $actionsOnThatEntity = $this->whatToDoWithEntity();
95
96 11
        if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
97 4
            $where = $this->getWhereConditionInConfig();
98 4
            $query = $this->runDelete($where, $pretend);
99 3
            ($returnRes === true ? array_push($queries, $query) : '');
100
        }
101
102 10
        if ($actionsOnThatEntity & self::UPDATE_TABLE) {
103 10
            $queries = array_merge(
104 10
                $queries,
105 10
                $this->updateData($limit, $returnRes, $pretend, $callback)
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 78 can also be of type null; however, Edyan\Neuralyzer\Anonymizer\DB::updateData() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
106
            );
107
        }
108
109 8
        if ($actionsOnThatEntity & self::INSERT_TABLE) {
110
            $queries = array_merge(
111
                $queries,
112
                $this->insertData($limit, $returnRes, $pretend, $callback)
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 78 can also be of type null; however, Edyan\Neuralyzer\Anonymizer\DB::insertData() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
113
            );
114
        }
115
116 8
        return $queries;
117
    }
118
119
120
    /**
121
     * Identify the primary key for a table
122
     *
123
     * @return string Field's name
124
     */
125 14
    private function getPrimaryKey()
126
    {
127 14
        $schema = $this->conn->getSchemaManager();
128 14
        $tableDetails = $schema->listTableDetails($this->entity);
129 14
        if ($tableDetails->hasPrimaryKey() === false) {
130 1
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
131
        }
132
133 13
        return $tableDetails->getPrimaryKey()->getColumns()[0];
134
    }
135
136
137
    /**
138
     * Retrieve columns list for a table with type and length
139
     *
140
     * @return array $cols
141
     */
142 13
    private function getTableCols()
143
    {
144 13
        $schema = $this->conn->getSchemaManager();
145 13
        $tableCols = $schema->listTableColumns($this->entity);
146 13
        $cols = [];
147 13
        foreach ($tableCols as $col) {
148 13
            $cols[$col->getName()] = [
149 13
                'length' => $col->getLength(),
150 13
                'type'   => $col->getType(),
151 13
                'unsigned' => $col->getUnsigned(),
152
            ];
153
        }
154
155 13
        return $cols;
156
    }
157
158
159
    /**
160
     * Execute the Update with Doctrine QueryBuilder
161
     *
162
     * @param  string $primaryKeyVal  Primary Key's Value
163
     * @return QueryBuilder           Doctrine DBAL QueryBuilder
164
     */
165 9
    private function prepareUpdate($primaryKeyVal): QueryBuilder
166
    {
167 9
        $data = $this->generateFakeData();
168
169 7
        $queryBuilder = $this->conn->createQueryBuilder();
170 7
        $queryBuilder = $queryBuilder->update($this->entity);
171 7
        foreach ($data as $field => $value) {
172 7
            $queryBuilder = $queryBuilder->set($field, $this->getCondition($field));
173 7
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
174
        }
175 7
        $queryBuilder = $queryBuilder->where("{$this->priKey} = :{$this->priKey}");
176 7
        $queryBuilder = $queryBuilder->setParameter(":{$this->priKey}", $primaryKeyVal);
177
178 7
        return $queryBuilder;
179
    }
180
181
    /**
182
     * Execute the Update with Doctrine QueryBuilder
183
     *
184
     * @return QueryBuilder       Doctrine DBAL QueryBuilder
185
     */
186
    private function prepareInsert(): QueryBuilder
187
    {
188
        $data = $this->generateFakeData();
189
190
        $queryBuilder = $this->conn->createQueryBuilder();
191
        $queryBuilder = $queryBuilder->insert($this->entity);
192
        foreach ($data as $field => $value) {
193
            $queryBuilder = $queryBuilder->setValue($field, ":$field");
194
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
195
        }
196
197
        return $queryBuilder;
198
    }
199
200
201
    /**
202
     * To debug, build the final SQL (can be approximative)
203
     * @param  QueryBuilder $queryBuilder
204
     * @return string
205
     */
206 6
    private function getRawSQL(QueryBuilder $queryBuilder)
207
    {
208 6
        $sql = $queryBuilder->getSQL();
209 6
        foreach ($queryBuilder->getParameters() as $parameter => $value) {
210 6
            $sql = str_replace($parameter, "'$value'", $sql);
211
        }
212
213 6
        return $sql;
214
    }
215
216
217
    /**
218
     * Execute the Delete with Doctrine Query Builder
219
     *
220
     * @param string $where
221
     * @param bool   $pretend
222
     *
223
     * @return string
224
     */
225 4
    private function runDelete(string $where, bool $pretend): string
226
    {
227 4
        $queryBuilder = $this->conn->createQueryBuilder();
228 4
        $queryBuilder = $queryBuilder->delete($this->entity);
229 4
        if (!empty($where)) {
230 3
            $queryBuilder = $queryBuilder->where($where);
231
        }
232 4
        $sql = $queryBuilder->getSQL();
233
234 4
        if ($pretend === true) {
235 1
            return $sql;
236
        }
237
238
        try {
239 3
            $queryBuilder->execute();
240 1
        } catch (\Exception $e) {
241 1
            throw new NeuralizerException('Query DELETE Error (' . $e->getMessage() . ')');
242
        }
243
244 2
        return $sql;
245
    }
246
247
248
    /**
249
     * Build the condition by casting the value if needed
250
     *
251
     * @param  string $field
252
     * @return string
253
     */
254 7
    private function getCondition(string $field): string
255
    {
256 7
        $type = strtolower($this->entityCols[$field]['type']);
257
258 7
        $integerCast = $this->getIntegerCast($field);
259
260 7
        $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
261
262
        $typeToCast = [
263 7
            'date'     => 'DATE',
264 7
            'datetime' => 'DATE',
265 7
            'time'     => 'TIME',
266 7
            'smallint' => $integerCast,
267 7
            'integer'  => $integerCast,
268 7
            'bigint'   => $integerCast,
269 7
            'float'    => 'DECIMAL',
270 7
            'decimal'  => 'DECIMAL',
271
        ];
272
273
        // No cast required
274 7
        if (!array_key_exists($type, $typeToCast)) {
275 7
            return $condition;
276
        }
277
278 7
        return "CAST($condition AS {$typeToCast[$type]})";
279
    }
280
281
282
    /**
283
     * Get the right CAST for an INTEGER
284
     *
285
     * @param  string $field
286
     * @return string
287
     */
288 7
    private function getIntegerCast(string $field): string
289
    {
290 7
        $driver = $this->getConn()->getDriver();
291 7
        if ($driver->getName() === 'pdo_mysql') {
292 7
            return $this->entityCols[$field]['unsigned'] === true ? 'UNSIGNED' : 'SIGNED';
293
        }
294
295
        return 'INTEGER';
296
    }
297
298
299
    /**
300
     * Update data of table
301
     *
302
     * @param  int    $limit
303
     * @param  bool   $returnRes
304
     * @param  bool   $pretend
305
     * @param  callable $callback
306
     * @return array             [description]
307
     */
308 10
    private function updateData(int $limit, bool $returnRes, bool $pretend, $callback): array
309
    {
310
        // I need to read line by line if I have to update the table
311
        // to make sure I do update by update (slower but no other choice for now)
312 10
        $rowNum = 0;
313 10
        $queries = [];
314
315 10
        $queryBuilder = $this->conn->createQueryBuilder();
316 10
        $rows = $queryBuilder->select($this->priKey)->from($this->entity)->execute();
317
318 10
        foreach ($rows as $row) {
0 ignored issues
show
Bug introduced by
The expression $rows of type object<Doctrine\DBAL\Driver\Statement>|integer is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
319 9
            $queryBuilder = $this->prepareUpdate($row[$this->priKey]);
320
321 7
            ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
322
323 7
            if ($pretend === false) {
324 4
                $queryBuilder->execute();
325
            }
326
327 7
            if (!is_null($callback)) {
328 3
                $callback(++$rowNum);
329
            }
330
331 7
            if ($limit > 0 && $rowNum >= $limit) {
332 7
                break;
333
            }
334
        }
335
336 8
        return $queries;
337
    }
338
339
340
    /**
341
     * Insert data into table
342
     *
343
     * @param  int    $limit
344
     * @param  bool   $returnRes
345
     * @param  bool   $pretend
346
     * @param  callable $callback
347
     * @return array             [description]
348
     */
349
    private function insertData(int $limit, bool $returnRes, bool $pretend, $callback): array
350
    {
351
        $queries = [];
352
353
        $queryBuilder = $this->conn->createQueryBuilder();
0 ignored issues
show
Unused Code introduced by
$queryBuilder is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
354
355
        for ($rowNum = 0; $rowNum < $limit; $rowNum++) {
356
            $queryBuilder = $this->prepareInsert();
357
358
            ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
359
360
            if ($pretend === false) {
361
                $queryBuilder->execute();
362
            }
363
364
            if (!is_null($callback)) {
365
                $callback($rowNum);
366
            }
367
        }
368
369
        return $queries;
370
    }
371
}
372