Passed
Push — master ( bf62c3...8c031b )
by Emmanuel
01:56
created

DB::updateData()   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 6.7272
c 0
b 0
f 0
ccs 15
cts 15
cp 1
cc 7
eloc 15
nc 17
nop 4
crap 7
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 31
     * Get Doctrine Connection
51
     * @return Doctrine\DBAL\Connection
52 31
     */
53 30
    public function getConn()
54
    {
55
        return $this->conn;
56
    }
57
58
59
    /**
60 22
     * Process an entity by reading / writing to the DB
61
     *
62 22
     * @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 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...
77
            throw new NeuralizerException("Table $entity does not exist");
78
        }
79
80
        $this->entity = $entity;
81
        $queries = [];
82
83 15
        $actionsOnThatEntity = $this->whatToDoWithEntity();
84 15
85 1
        if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
86
            $where = $this->getWhereConditionInConfig();
87
            $query = $this->runDelete($where, $pretend);
88 14
            ($returnRes === true ? array_push($queries, $query) : '');
89 14
        }
90 13
91
        if ($actionsOnThatEntity & self::UPDATE_TABLE) {
92 13
            // 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 13
            $rowNum = 0;
95
96 11
            $key = $this->getPrimaryKey();
97 4
            $this->entityCols = $this->getTableCols();
98 4
99 3
            $queryBuilder = $this->conn->createQueryBuilder();
100
            $rows = $queryBuilder->select($key)->from($this->entity)->execute();
101
102 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...
103 10
                $queryBuilder = $this->prepareUpdate($key, $row[$key]);
104 10
105 10
                ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
106
107
                if ($pretend === false) {
108
                    $queryBuilder->execute();
109 8
                }
110
111
                if (!is_null($callback)) {
112
                    $callback(++$rowNum);
113
                }
114
            }
115
        }
116 8
117
        return $queries;
118
    }
119
120
121
    /**
122
     * Identify the primary key for a table
123
     *
124
     * @return string Field's name
125 14
     */
126
    private function getPrimaryKey()
127 14
    {
128 14
        $schema = $this->conn->getSchemaManager();
129 14
        $tableDetails = $schema->listTableDetails($this->entity);
130 1
        if ($tableDetails->hasPrimaryKey() === false) {
131
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
132
        }
133 13
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 13
     */
143
    private function getTableCols()
144 13
    {
145 13
        $schema = $this->conn->getSchemaManager();
146 13
        $tableCols = $schema->listTableColumns($this->entity);
147 13
        $cols = [];
148 13
        foreach ($tableCols as $col) {
149 13
            $cols[$col->getName()] = [
150 13
                'length' => $col->getLength(),
151 13
                'type'   => $col->getType(),
152
                'unsigned' => $col->getUnsigned(),
153
            ];
154
        }
155 13
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 9
     * @return string                 Doctrine DBAL QueryBuilder
166
     */
167 9
    private function prepareUpdate(string $primaryKey, $primaryKeyVal): QueryBuilder
168
    {
169 7
        $data = $this->generateFakeData();
170 7
171 7
        $queryBuilder = $this->conn->createQueryBuilder();
172 7
        $queryBuilder = $queryBuilder->update($this->entity);
173 7
        foreach ($data as $field => $value) {
174
            $queryBuilder = $queryBuilder->set($field, $this->getCondition($field));
175 7
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
176 7
        }
177
        $queryBuilder = $queryBuilder->where("$primaryKey = :$primaryKey");
178 7
        $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 6
     * @return string
207
     */
208 6
    private function runDelete(string $where, bool $pretend): string
209 6
    {
210 6
        $queryBuilder = $this->conn->createQueryBuilder();
211
        $queryBuilder = $queryBuilder->delete($this->entity);
212
        if (!empty($where)) {
213 6
            $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 4
        }
226
227 4
        return $sql;
228 4
    }
229 4
230 3
231
    /**
232 4
     * Build the condition by casting the value if needed
233
     *
234 4
     * @param  string $field
235 1
     * @return string
236
     */
237
    private function getCondition(string $field): string
238
    {
239 3
        $type = strtolower($this->entityCols[$field]['type']);
240 1
241 1
        $integerCast = $this->getIntegerCast($field);
242
243
        $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
244 2
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 7
255
        ];
256 7
257
        // No cast required
258 7
        if (!array_key_exists($type, $typeToCast)) {
259
            return $condition;
260 7
        }
261
262
        return "CAST($condition AS {$typeToCast[$type]})";
263 7
    }
264 7
265 7
266 7
    /**
267 7
     * Get the right CAST for an INTEGER
268 7
     *
269 7
     * @param  string $field
270 7
     * @return string
271
     */
272
    private function getIntegerCast(string $field): string
273
    {
274 7
        $driver = $this->getConn()->getDriver();
275 7
        if ($driver->getName() === 'pdo_mysql') {
276
            return $this->entityCols[$field]['unsigned'] === true ? 'UNSIGNED' : 'SIGNED';
277
        }
278 7
279
        return 'INTEGER';
280
    }
281
}
282