Completed
Push — master ( 87dd25...1ab114 )
by Emmanuel
02:39
created

DB::updateData()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 14
cts 14
cp 1
rs 8.439
c 0
b 0
f 0
cc 6
eloc 17
nc 18
nop 3
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\Anonymizer;
19
20
use Doctrine\DBAL\Connection;
21
use Doctrine\DBAL\Configuration as DbalConfiguration;
22
use Doctrine\DBAL\DriverManager as DbalDriverManager;
23
use Doctrine\DBAL\Query\QueryBuilder;
24
use Edyan\Neuralyzer\Exception\NeuralizerException;
25
26
/**
27
 * Implement AbstractAnonymizer for DB, to read and write data via Doctrine DBAL
28
 */
29
class DB extends AbstractAnonymizer
30
{
31
    /**
32
     * Doctrine DB Adapter
33
     *
34
     * @var Connection
35
     */
36
    private $conn;
37
38
39
    /**
40
     * Primary Key
41
     *
42
     * @var string
43
     */
44
    private $priKey;
45
46
    /**
47
     * Limit the number of updates or create
48
     *
49
     * @var int
50
     */
51
    private $limit = 0;
52
53
    /**
54
     * Init connection
55
     *
56
     * @param $params   Parameters to send to Doctrine DB
57
     */
58 34
    public function __construct(array $params)
59
    {
60 34
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
61 33
    }
62
63
64
    /**
65
     * Get Doctrine Connection
66
     *
67
     * @return Connection
68
     */
69 22
    public function getConn(): Connection
70
    {
71 22
        return $this->conn;
72
    }
73
74
75
    /**
76
     * Set the limit for updates and creates
77
     *
78
     * @param int $limit
79
     * @return DB
80 5
     */
81
    public function setLimit(int $limit): DB
82 5
    {
83 5
        $this->limit = $limit;
84
85
        return $this;
86
    }
87
88
89
    /**
90
     * Process an entity by reading / writing to the DB
91
     *
92
     * @param string        $entity
93
     * @param callable|null $callback
94
     * @param bool          $pretend
95
     * @param bool          $returnRes
96 18
     *
97
     * @return void|array
98
     */
99
    public function processEntity(
100
        string $entity,
101
        callable $callback = null,
102 18
        bool $pretend = true,
103 18
        bool $returnRes = false
104 1
    ): array {
105
        $schema = $this->conn->getSchemaManager();
106
        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...
107 17
            throw new NeuralizerException("Table $entity does not exist");
108 17
        }
109 16
110
        $this->entity = $entity;
111 16
        $this->priKey = $this->getPrimaryKey();
112
        $this->entityCols = $this->getTableCols();
113 16
114
        $queries = [];
115 14
116 4
        $actionsOnThatEntity = $this->whatToDoWithEntity();
117 4
118 3
        if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
119
            $where = $this->getWhereConditionInConfig();
120
            $query = $this->runDelete($where, $pretend);
121 13
            ($returnRes === true ? array_push($queries, $query) : '');
122 10
        }
123 10
124 10
        if ($actionsOnThatEntity & self::UPDATE_TABLE) {
125
            $queries = array_merge(
126
                $queries,
127
                $this->updateData($returnRes, $pretend, $callback)
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 101 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...
128 11
            );
129 3
        }
130 3
131 3
        if ($actionsOnThatEntity & self::INSERT_TABLE) {
132
            $queries = array_merge(
133
                $queries,
134
                $this->insertData($returnRes, $pretend, $callback)
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 101 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...
135 11
            );
136
        }
137
138
        return $queries;
139
    }
140
141
142
    /**
143
     * Identify the primary key for a table
144 17
     *
145
     * @return string Field's name
146 17
     */
147 17
    private function getPrimaryKey(): string
148 17
    {
149 1
        $schema = $this->conn->getSchemaManager();
150
        $tableDetails = $schema->listTableDetails($this->entity);
151
        if ($tableDetails->hasPrimaryKey() === false) {
152 16
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
153
        }
154
155
        return $tableDetails->getPrimaryKey()->getColumns()[0];
156
    }
157
158
159
    /**
160
     * Retrieve columns list for a table with type and length
161 16
     *
162
     * @return array $cols
163 16
     */
164 16
    private function getTableCols(): array
165 16
    {
166 16
        $schema = $this->conn->getSchemaManager();
167 16
        $tableCols = $schema->listTableColumns($this->entity);
168 16
        $cols = [];
169 16
        foreach ($tableCols as $col) {
170 16
            $cols[$col->getName()] = [
171
                'length' => $col->getLength(),
172
                'type'   => $col->getType(),
173
                'unsigned' => $col->getUnsigned(),
174 16
            ];
175
        }
176
177
        return $cols;
178
    }
179
180
181
    /**
182
     * Execute the Update with Doctrine QueryBuilder
183
     *
184 9
     * @param  string $primaryKeyVal  Primary Key's Value
185
     * @return QueryBuilder           Doctrine DBAL QueryBuilder
186 9
     */
187
    private function prepareUpdate($primaryKeyVal): QueryBuilder
188 7
    {
189 7
        $data = $this->generateFakeData();
190 7
191 7
        $queryBuilder = $this->conn->createQueryBuilder();
192 7
        $queryBuilder = $queryBuilder->update($this->entity);
193
        foreach ($data as $field => $value) {
194 7
            $queryBuilder = $queryBuilder->set($field, $this->getCondition($field));
195 7
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
196
        }
197 7
        $queryBuilder = $queryBuilder->where("{$this->priKey} = :{$this->priKey}");
198
        $queryBuilder = $queryBuilder->setParameter(":{$this->priKey}", $primaryKeyVal);
199
200
        return $queryBuilder;
201
    }
202
203
    /**
204
     * Execute the Update with Doctrine QueryBuilder
205 3
     *
206
     * @return QueryBuilder       Doctrine DBAL QueryBuilder
207 3
     */
208
    private function prepareInsert(): QueryBuilder
209 3
    {
210 3
        $data = $this->generateFakeData();
211 3
212 3
        $queryBuilder = $this->conn->createQueryBuilder();
213 3
        $queryBuilder = $queryBuilder->insert($this->entity);
214
        foreach ($data as $field => $value) {
215
            $queryBuilder = $queryBuilder->setValue($field, ":$field");
216 3
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
217
        }
218
219
        return $queryBuilder;
220
    }
221
222
223
    /**
224
     * To debug, build the final SQL (can be approximative)
225 8
     * @param  QueryBuilder $queryBuilder
226
     * @return string
227 8
     */
228 8
    private function getRawSQL(QueryBuilder $queryBuilder)
229 8
    {
230
        $sql = $queryBuilder->getSQL();
231
        foreach ($queryBuilder->getParameters() as $parameter => $value) {
232 8
            $sql = str_replace($parameter, "'$value'", $sql);
233
        }
234
235
        return $sql;
236
    }
237
238
239
    /**
240
     * Execute the Delete with Doctrine Query Builder
241
     *
242
     * @param string $where
243
     * @param bool   $pretend
244 4
     *
245
     * @return string
246 4
     */
247 4
    private function runDelete(string $where, bool $pretend): string
248 4
    {
249 3
        $queryBuilder = $this->conn->createQueryBuilder();
250
        $queryBuilder = $queryBuilder->delete($this->entity);
251 4
        if (!empty($where)) {
252
            $queryBuilder = $queryBuilder->where($where);
253 4
        }
254 1
        $sql = $queryBuilder->getSQL();
255
256
        if ($pretend === true) {
257
            return $sql;
258 3
        }
259 1
260 1
        try {
261
            $queryBuilder->execute();
262
        } catch (\Exception $e) {
263 2
            throw new NeuralizerException('Query DELETE Error (' . $e->getMessage() . ')');
264
        }
265
266
        return $sql;
267
    }
268
269
270
    /**
271
     * Build the condition by casting the value if needed
272
     *
273 7
     * @param  string $field
274
     * @return string
275 7
     */
276
    private function getCondition(string $field): string
277 7
    {
278
        $type = strtolower($this->entityCols[$field]['type']);
279 7
280
        $integerCast = $this->getIntegerCast($field);
281
282 7
        $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
283 7
284 7
        $typeToCast = [
285 7
            'date'     => 'DATE',
286 7
            'datetime' => 'DATE',
287 7
            'time'     => 'TIME',
288 7
            'smallint' => $integerCast,
289 7
            'integer'  => $integerCast,
290
            'bigint'   => $integerCast,
291
            'float'    => 'DECIMAL',
292
            'decimal'  => 'DECIMAL',
293 7
        ];
294 7
295
        // No cast required
296
        if (!array_key_exists($type, $typeToCast)) {
297 7
            return $condition;
298
        }
299
300
        return "CAST($condition AS {$typeToCast[$type]})";
301
    }
302
303
304
    /**
305
     * Get the right CAST for an INTEGER
306
     *
307 7
     * @param  string $field
308
     * @return string
309 7
     */
310 7
    private function getIntegerCast(string $field): string
311 7
    {
312
        $driver = $this->getConn()->getDriver();
313
        if ($driver->getName() === 'pdo_mysql') {
314
            return $this->entityCols[$field]['unsigned'] === true ? 'UNSIGNED' : 'SIGNED';
315
        }
316
317
        return 'INTEGER';
318
    }
319
320
321
    /**
322
     * Update data of table
323
     *
324
     * @param  bool   $returnRes
325
     * @param  bool   $pretend
326 10
     * @param  callable $callback
327
     * @return array
328
     */
329
    private function updateData(bool $returnRes, bool $pretend, $callback): array
330 10
    {
331 10
        // I need to read line by line if I have to update the table
332
        // to make sure I do update by update (slower but no other choice for now)
333 10
        $rowNum = 0;
334 10
        $queries = [];
335
336 10
        $queryBuilder = $this->conn->createQueryBuilder();
337 9
        $query = $queryBuilder->select($this->priKey)->from($this->entity);
338
        if ($this->limit > 0) {
339 7
            $query = $query->setMaxResults($this->limit);
340
        }
341 7
342 4
        $rows = $query->execute();
343
        $rows->setFetchMode(\Doctrine\DBAL\FetchMode::ASSOCIATIVE);
344
345 7
        while ($row = $rows->fetch()) {
346 3
            $queryBuilder = $this->prepareUpdate($row[$this->priKey]);
347
348
            ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
349 7
350 7
            if ($pretend === false) {
351
                $queryBuilder->execute();
352
            }
353
354 8
            if (!is_null($callback)) {
355
                $callback(++$rowNum);
356
            }
357
        }
358
359
        return $queries;
360
    }
361
362
363
    /**
364
     * Insert data into table
365
     *
366 3
     * @param  bool   $returnRes
367
     * @param  bool   $pretend
368 3
     * @param  callable $callback
369
     * @return array
370 3
     */
371
    private function insertData(bool $returnRes, bool $pretend, $callback): array
372 3
    {
373 3
        $queries = [];
374
375 3
        $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...
376
377 3
        for ($rowNum = 1; $rowNum <= $this->limit; $rowNum++) {
378 2
            $queryBuilder = $this->prepareInsert();
379
380
            ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
381 3
382 1
            if ($pretend === false) {
383
                $queryBuilder->execute();
384
            }
385
386 3
            if (!is_null($callback)) {
387
                $callback($rowNum);
388
            }
389
        }
390
391
        return $queries;
392
    }
393
}
394