Test Failed
Push — master ( 359bf7...72eda2 )
by Emmanuel
02:11
created

DB::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 4
cp 0
cc 1
eloc 2
nc 1
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
     * Init connection
40
     *
41
     * @param $params   Parameters to send to Doctrine DB
42
     */
43
    public function __construct(array $params)
44
    {
45
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
46
    }
47
48
49
    /**
50
     * Get Doctrine Connection
51
     * @return Doctrine\DBAL\Connection
52
     */
53
    public function getConn()
54
    {
55
        return $this->conn;
56
    }
57
58
59
    /**
60
     * Process an entity by reading / writing to the DB
61
     *
62
     * @param string        $entity
63
     * @param callable|null $callback
64
     * @param bool          $pretend
65
     * @param bool          $returnRes
66
     *
67
     * @return void|array
68
     */
69
    public function processEntity(
70
        string $entity,
71
        callable $callback = null,
72
        bool $pretend = true,
73
        bool $returnRes = false
74
    ): array {
75
        $schema = $this->conn->getSchemaManager();
76
        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...
77
            throw new NeuralizerException("Table $entity does not exist");
78
        }
79
80
        $this->entity = $entity;
81
        $queries = [];
82
83
        $actionsOnThatEntity = $this->whatToDoWithEntity();
84
85
        if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
86
            $where = $this->getWhereConditionInConfig();
87
            $query = $this->runDelete($where, $pretend);
88
            ($returnRes === true ? array_push($queries, $query) : '');
89
        }
90
91
        if ($actionsOnThatEntity & self::UPDATE_TABLE) {
92
            // I need to read line by line if I have to update the table
93
            // to make sure I do update by update (slower but no other choice for now)
94
            $rowNum = 0;
95
96
            $key = $this->getPrimaryKey();
97
            $this->entityCols = $this->getTableCols();
98
99
            $queryBuilder = $this->conn->createQueryBuilder();
100
            $rows = $queryBuilder->select($key)->from($this->entity)->execute();
101
102
            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...
103
                $queryBuilder = $this->prepareUpdate($key, $row[$key]);
104
105
                ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
106
107
                if ($pretend === false) {
108
                    $queryBuilder->execute();
109
                }
110
111
                if (!is_null($callback)) {
112
                    $callback(++$rowNum);
113
                }
114
            }
115
        }
116
117
        return $queries;
118
    }
119
120
121
    /**
122
     * Identify the primary key for a table
123
     *
124
     * @return string Field's name
125
     */
126
    private function getPrimaryKey()
127
    {
128
        $schema = $this->conn->getSchemaManager();
129
        $tableDetails = $schema->listTableDetails($this->entity);
130
        if ($tableDetails->hasPrimaryKey() === false) {
131
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
132
        }
133
134
        return $tableDetails->getPrimaryKey()->getColumns()[0];
135
    }
136
137
138
    /**
139
     * Retrieve columns list for a table with type and length
140
     *
141
     * @return array $cols
142
     */
143
    private function getTableCols()
144
    {
145
        $schema = $this->conn->getSchemaManager();
146
        $tableCols = $schema->listTableColumns($this->entity);
147
        $cols = [];
148
        foreach ($tableCols as $col) {
149
            $cols[$col->getName()] = [
150
                'length' => $col->getLength(),
151
                'type'   => $col->getType(),
152
                'unsigned' => $col->getUnsigned(),
153
            ];
154
        }
155
156
        return $cols;
157
    }
158
159
160
    /**
161
     * Execute the Update with Doctrine QueryBuilder
162
     *
163
     * @param  string $primaryKey
164
     * @param  string $primaryKeyVal  Primary Key's Value
165
     * @return string                 Doctrine DBAL QueryBuilder
166
     */
167
    private function prepareUpdate(string $primaryKey, $primaryKeyVal): QueryBuilder
168
    {
169
        $data = $this->generateFakeData();
170
171
        $queryBuilder = $this->conn->createQueryBuilder();
172
        $queryBuilder = $queryBuilder->update($this->entity);
173
        foreach ($data as $field => $value) {
174
            $queryBuilder = $queryBuilder->set($field, $this->getCondition($field));
175
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
176
        }
177
        $queryBuilder = $queryBuilder->where("$primaryKey = :$primaryKey");
178
        $queryBuilder = $queryBuilder->setParameter(":$primaryKey", $primaryKeyVal);
179
180
        return $queryBuilder;
181
    }
182
183
184
    /**
185
     * To debug, build the final SQL (can be approximative)
186
     * @param  QueryBuilder $queryBuilder
187
     * @return string
188
     */
189
    private function getRawSQL(QueryBuilder $queryBuilder)
190
    {
191
        $sql = $queryBuilder->getSQL();
192
        foreach ($queryBuilder->getParameters() as $parameter => $value) {
193
            $sql = str_replace($parameter, "'$value'", $sql);
194
        }
195
196
        return $sql;
197
    }
198
199
200
    /**
201
     * Execute the Delete with Doctrine Query Builder
202
     *
203
     * @param string $where
204
     * @param bool   $pretend
205
     *
206
     * @return string
207
     */
208
    private function runDelete(string $where, bool $pretend): string
209
    {
210
        $queryBuilder = $this->conn->createQueryBuilder();
211
        $queryBuilder = $queryBuilder->delete($this->entity);
212
        if (!empty($where)) {
213
            $queryBuilder = $queryBuilder->where($where);
214
        }
215
        $sql = $queryBuilder->getSQL();
216
217
        if ($pretend === true) {
218
            return $sql;
219
        }
220
221
        try {
222
            $queryBuilder->execute();
223
        } catch (\Exception $e) {
224
            throw new NeuralizerException('Query DELETE Error (' . $e->getMessage() . ')');
225
        }
226
227
        return $sql;
228
    }
229
230
231
    /**
232
     * Build the condition by casting the value if needed
233
     *
234
     * @param  string $field
235
     * @return string
236
     */
237
    private function getCondition(string $field): string
238
    {
239
        $type = strtolower($this->entityCols[$field]['type']);
240
241
        $integerCast = $this->getIntegerCast($field);
242
243
        $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
244
245
        $typeToCast = [
246
            'date'     => 'DATE',
247
            'datetime' => 'DATE',
248
            'time'     => 'TIME',
249
            'smallint' => $integerCast,
250
            'integer'  => $integerCast,
251
            'bigint'   => $integerCast,
252
            'float'    => 'DECIMAL',
253
            'decimal'  => 'DECIMAL',
254
255
        ];
256
257
        // No cast required
258
        if (!array_key_exists($type, $typeToCast)) {
259
            return $condition;
260
        }
261
262
        return "CAST($condition AS {$typeToCast[$type]})";
263
    }
264
265
266
    /**
267
     * Get the right CAST for an INTEGER
268
     *
269
     * @param  string $field
270
     * @return string
271
     */
272
    private function getIntegerCast(string $field): string
273
    {
274
        $driver = $this->getConn()->getDriver();
275
        if ($driver->getName() === 'pdo_mysql') {
276
            return $this->entityCols[$field]['unsigned'] === true ? 'UNSIGNED' : 'SIGNED';
277
        }
278
279
        return 'INTEGER';
280
    }
281
}
282