Passed
Push — master ( bbf0da...682cd1 )
by Emmanuel
02:36
created

DB   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 77.61%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 10
dl 0
loc 369
ccs 104
cts 134
cp 0.7761
rs 8.295
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A getConn() 0 4 1
A setMode() 0 15 3
B processEntity() 0 39 6
A runDelete() 0 17 3
C updateData() 0 39 7
B doUpdateByQueries() 0 23 5
A doBatchUpdate() 0 15 4
A insertData() 0 16 4
A doInsertByQueries() 0 19 4
A doBatchInsert() 0 5 1
A loadDataInBatch() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like DB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DB, and based on these observations, apply Extract Interface, too.

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\Helper\DB as DBHelper;
25
use Edyan\Neuralyzer\Exception\NeuralizerException;
26
use Edyan\Neuralyzer\Utils\CSVWriter;
27
use Edyan\Neuralyzer\Utils\DBUtils;
28
29
/**
30
 * Implement AbstractAnonymizer for DB, to read and write data via Doctrine DBAL
31
 */
32
class DB extends AbstractAnonymizer
33
{
34
    /**
35
     * Doctrine DB Adapter
36
     * @var Connection
37
     */
38
    private $conn;
39
40
    /**
41
     * A helper for the current driver
42
     * @var DBHelper\AbstractDBHelper
43
     */
44
    private $dbHelper;
45
46
    /**
47
     * Various generic utils
48
     * @var DBUtils
49
     */
50
    private $dbUtils;
51
52
    /**
53
     * Primary Key
54
     * @var string
55
     */
56
    private $priKey;
57
58
    /**
59
     * Define the way we update / insert data
60
     * @var string
61
     */
62
    private $mode = 'queries';
63
64
    /**
65
     * Contains queries if returnRes is true
66
     * @var array
67
     */
68
    private $queries = [];
69
70
    /**
71
     * File resource for the csv (batch mode)
72
     * @var CSVWriter
73
     */
74
    private $csv;
75
76
    /**
77
     * Define available update modes
78
     * @var array
79
     */
80
    private $updateMode = [
81
        'queries' => 'doUpdateByQueries',
82
        'batch' => 'doBatchUpdate'
83
    ];
84
85
    /**
86
     * Define available insert modes
87
     * @var array
88
     */
89
    private $insertMode = [
90
        'queries' => 'doInsertByQueries',
91
        'batch' => 'doBatchInsert'
92
    ];
93
94
    /**
95
     * Init connection
96
     *
97
     * @param $params   Parameters to send to Doctrine DB
98
     */
99
    public function __construct(array $params)
100 10
    {
101
        $dbHelperClass = DBHelper\DriverGuesser::getDBHelper($params['driver']);
102
103 10
        // Set specific options
104 10
        $params['driverOptions'] = $dbHelperClass::getDriverOptions();
105
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
106
        $this->conn->setFetchMode(\Doctrine\DBAL\FetchMode::ASSOCIATIVE);
107 10
108 10
        $this->dbUtils = new DBUtils($this->conn);
109
        $this->dbHelper = new $dbHelperClass($this->conn);
110 10
    }
111 10
112
113
    /**
114
     * Get Doctrine Connection
115
     *
116
     * @return Connection
117
     */
118
    public function getConn(): Connection
119
    {
120
        return $this->conn;
121
    }
122
123
124
    /**
125
     * Set the mode for update / insert
126
     * @param string $mode
127
     * @return DB
128
     */
129
    public function setMode(string $mode): DB
130 2
    {
131
        if (!in_array($mode, ['queries', 'batch'])) {
132 2
            throw new NeuralizerException('Mode could be only queries or batch');
133 1
        }
134
135
        if ($mode === 'batch') {
136 1
            $this->csv = new CSVWriter();
137 1
            $this->csv->setCsvControl('|', $this->dbHelper->getEnclosureForCSV());
138 1
        }
139
140 1
        $this->mode = $mode;
141 1
142 1
        return $this;
143
    }
144
145 1
146
    /**
147 1
     * Process an entity by reading / writing to the DB
148
     *
149
     * @param string        $entity
150
     * @param callable|null $callback
151
     *
152
     * @return void|array
153
     */
154
    public function processEntity(string $entity, callable $callback = null): array
155
    {
156
        $this->dbUtils->assertTableExists($entity);
157
158
        $this->priKey = $this->dbUtils->getPrimaryKey($entity);
159 9
        $this->entityCols = $this->dbUtils->getTableCols($entity);
160
        $this->entity = $entity;
161 9
162
        $actionsOnThatEntity = $this->whatToDoWithEntity();
163 8
        $this->queries = [];
164 7
165 7
        // Wrap everything in a transaction
166
        try {
167 7
            $this->conn->beginTransaction();
168 5
169
            if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
170
                $where = $this->getWhereConditionInConfig();
171
                $query = $this->runDelete($where);
172 5
                ($this->returnRes === true ? array_push($this->queries, $query) : '');
173
            }
174 5
175 3
            if ($actionsOnThatEntity & self::UPDATE_TABLE) {
176 3
                $this->updateData($callback);
177 2
            }
178
179
            if ($actionsOnThatEntity & self::INSERT_TABLE) {
180 4
                $this->insertData($callback);
181 3
            }
182
183
            $this->conn->commit();
184 4
        } catch (\Exception $e) {
185 1
            $this->conn->rollback();
186
            $this->conn->close(); // To avoid locks
187
188 4
            throw $e;
189 1
        }
190 1
191 1
        return $this->queries;
192
    }
193 1
194
195
    /**
196 4
     * Execute the Delete with Doctrine Query Builder
197
     *
198
     * @param string $where
199
     *
200
     * @return string
201
     */
202
    private function runDelete(string $where): string
203
    {
204
        $queryBuilder = $this->conn->createQueryBuilder();
205
        $queryBuilder = $queryBuilder->delete($this->entity);
206
        if (!empty($where)) {
207 3
            $queryBuilder = $queryBuilder->where($where);
208
        }
209 3
        $sql = $queryBuilder->getSQL();
210 3
211 3
        if ($this->pretend === true) {
212 2
            return $sql;
213
        }
214 3
215
        $queryBuilder->execute();
216 3
217 1
        return $sql;
218
    }
219
220 2
221
    /**
222 1
     * Update data of table
223
     *
224
     * @param  callable $callback
225
     */
226
    private function updateData($callback = null): void
227
    {
228
        $queryBuilder = $this->conn->createQueryBuilder();
229
        if ($this->limit === 0) {
230
            $this->setLimit($this->dbUtils->countResults($this->entity));
231 3
        }
232
233 3
        $startAt = 0; // The first part of the limit (offset)
234 3
        $num = 0; // The number of rows updated
235 3
        while ($num < $this->limit) {
236
            $rows = $queryBuilder
237
                        ->select('*')->from($this->entity)
238 3
                        ->setFirstResult($startAt)->setMaxResults($this->batchSize)
239 3
                        ->orderBy($this->priKey)
240 3
                        ->execute();
241
242 3
            // I need to read line by line if I have to update the table
243 3
            // to make sure I do update by update (slower but no other choice for now)
244 3
            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...
245 3
                // Call the right method according to the mode
246
                $this->{$this->updateMode[$this->mode]}($row);
247
248
                if (!is_null($callback)) {
249 3
                    $callback(++$num);
250
                }
251 3
                // Have to exit now as we have reached the max
252
                if ($num >= $this->limit) {
253 3
                    break 2;
254 1
                }
255
            }
256
            // Move the offset
257 3
            // Make sure the loop ends if we have nothing to process
258 3
            $num = $startAt += $this->batchSize;
259
        }
260
        // Run a final method if defined
261
        if ($this->mode === 'batch') {
262
            $this->loadDataInBatch('update');
263 2
        }
264
    }
265
266 3
267
    /**
268
     * Execute the Update with Doctrine QueryBuilder
269 3
     * @SuppressWarnings("unused") - Used dynamically
270
     * @param  array $row  Full row
271
     */
272
    private function doUpdateByQueries(array $row): void
273
    {
274
        $data = $this->generateFakeData();
275
276
        $queryBuilder = $this->conn->createQueryBuilder();
277 3
        $queryBuilder = $queryBuilder->update($this->entity);
278
        foreach ($data as $field => $value) {
279 3
            $value = empty($row[$field]) ? '' : $value;
280
            $condition = $this->dbUtils->getCondition($field, $this->entityCols[$field]);
281 3
            $queryBuilder = $queryBuilder->set($field, $condition);
282 3
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
283 3
        }
284 3
        $queryBuilder = $queryBuilder->where("{$this->priKey} = :{$this->priKey}");
285 3
        $queryBuilder = $queryBuilder->setParameter(":{$this->priKey}", $row[$this->priKey]);
286 3
287 3
        $this->returnRes === true ?
288
            array_push($this->queries, $this->dbUtils->getRawSQL($queryBuilder)) :
289 3
            '';
290
291 3
        if ($this->pretend === false) {
292 3
            $queryBuilder->execute();
293
        }
294 3
    }
295 3
296
297
    /**
298 3
     * Write the line required for a later LOAD DATA (or \copy)
299
     * @SuppressWarnings("unused") - Used dynamically
300
     * @param  array $row  Full row
301 3
     */
302
    private function doBatchUpdate(array $row): void
303
    {
304
        $fakeData = $this->generateFakeData();
305
        $data = [];
306
        // Go trough all fields, and take a value by priority
307
        foreach (array_keys($this->entityCols) as $field) {
308
            // First take the fake data
309
            $data[$field] = $row[$field];
310
            if (!empty($row[$field]) && array_key_exists($field, $fakeData)) {
311
                $data[$field] = $fakeData[$field];
312
            }
313
        }
314
315
        $this->csv->write($data);
316
    }
317
318
319
    /**
320
     * Insert data into table
321
     * @param  callable $callback
322
     */
323
    private function insertData($callback = null): void
324
    {
325
        for ($rowNum = 1; $rowNum <= $this->limit; $rowNum++) {
326
            // Call the right method according to the mode
327
            $this->{$this->insertMode[$this->mode]}($rowNum);
328
329
            if (!is_null($callback)) {
330 1
                $callback($rowNum);
331
            }
332 1
        }
333
334 1
        // Run a final method if defined
335
        if ($this->mode === 'batch') {
336 1
            $this->loadDataInBatch('insert');
337 1
        }
338
    }
339
340
341
    /**
342 1
     * Execute an INSERT with Doctrine QueryBuilder
343 1
     * @SuppressWarnings("unused") - Used dynamically
344
     */
345 1
    private function doInsertByQueries(): void
346
    {
347
        $data = $this->generateFakeData();
348
349
        $queryBuilder = $this->conn->createQueryBuilder();
350
        $queryBuilder = $queryBuilder->insert($this->entity);
351
        foreach ($data as $field => $value) {
352
            $queryBuilder = $queryBuilder->setValue($field, ":$field");
353
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
354
        }
355
356
        $this->returnRes === true ?
357
            array_push($this->queries, $this->dbUtils->getRawSQL($queryBuilder)) :
358
            '';
359
360
        if ($this->pretend === false) {
361
            $queryBuilder->execute();
362
        }
363
    }
364
365
366
    /**
367
     * Write the line required for a later LOAD DATA (or \copy)
368
     * @SuppressWarnings("unused") - Used dynamically
369
     */
370
    private function doBatchInsert(): void
371
    {
372
        $data = $this->generateFakeData();
373
        $this->csv->write($data);
374
    }
375
376
377 1
    /**
378
     * If a file has been created for the batch mode, destroy it
379 1
     * @SuppressWarnings("unused") - Used dynamically
380 1
     * @param string $mode "update" or "insert"
381 1
     */
382
    private function loadDataInBatch(string $mode): void
383
    {
384
        $fields = array_keys($this->configEntites[$this->entity]['cols']);
385
        // Replace by all fields if update as we have to load everything
386
        if ($mode === 'update') {
387
            $fields = array_keys($this->entityCols);
388
        }
389 1
390
        // Load the data from the helper, only if pretend is false
391 1
        $filename = $this->csv->getRealPath();
392 1
        $this->dbHelper->setPretend($this->pretend);
393
        $sql = $this->dbHelper->loadData($this->entity, $filename, $fields, $mode);
394 1
395
        $this->returnRes === true ? array_push($this->queries, $sql) : '';
396 1
397
        // Destroy the file
398
        unlink($this->csv->getRealPath());
399
    }
400
}
401