Passed
Push — master ( c10078...a55606 )
by Emmanuel
01:54
created

DB::runDelete()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 12
cts 12
cp 1
rs 9.0534
c 0
b 0
f 0
nc 6
cc 4
eloc 13
nop 2
crap 4
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 31
    public function __construct(array $params)
44
    {
45 31
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
46 30
    }
47
48
49
    /**
50
     * Get Doctrine Connection
51
     * @return Doctrine\DBAL\Connection
52
     */
53 22
    public function getConn()
54
    {
55 22
        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 15
    public function processEntity(
70
        string $entity,
71
        callable $callback = null,
72
        bool $pretend = true,
73
        bool $returnRes = false
74
    ): array {
75 15
        $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 1
            throw new NeuralizerException("Table $entity does not exist");
78
        }
79
80 14
        $this->entity = $entity;
81 14
        $queries = [];
82
83 14
        $actionsOnThatEntity = $this->whatToDoWithEntity();
84
85 12
        if ($actionsOnThatEntity & self::TRUNCATE_TABLE) {
86 4
            $where = $this->getWhereConditionInConfig();
87 4
            $query = $this->runDelete($where, $pretend);
88 3
            ($returnRes === true ? array_push($queries, $query) : '');
89
        }
90
91 11
        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 11
            $rowNum = 0;
95
96 11
            $key = $this->getPrimaryKey();
97 10
            $this->entityCols = $this->getTableCols();
98
99 10
            $queryBuilder = $this->conn->createQueryBuilder();
100 10
            $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 9
                $queryBuilder = $this->prepareUpdate($key, $row[$key]);
104
105 7
                ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
106
107 7
                if ($pretend === false) {
108 4
                    $queryBuilder->execute();
109
                }
110
111 7
                if (!is_null($callback)) {
112 7
                    $callback(++$rowNum);
113
                }
114
            }
115
        }
116
117 8
        return $queries;
118
    }
119
120
121
    /**
122
     * Identify the primary key for a table
123
     *
124
     * @return string Field's name
125
     */
126 11
    private function getPrimaryKey()
127
    {
128 11
        $schema = $this->conn->getSchemaManager();
129 11
        $tableDetails = $schema->listTableDetails($this->entity);
130 11
        if ($tableDetails->hasPrimaryKey() === false) {
131 1
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
132
        }
133
134 10
        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 10
    private function getTableCols()
144
    {
145 10
        $schema = $this->conn->getSchemaManager();
146 10
        $tableCols = $schema->listTableColumns($this->entity);
147 10
        $cols = [];
148 10
        foreach ($tableCols as $col) {
149 10
            $cols[$col->getName()] = [
150 10
                'length' => $col->getLength(),
151 10
                'type'   => $col->getType(),
152 10
                'unsigned' => $col->getUnsigned(),
153
            ];
154
        }
155
156 10
        return $cols;
157
    }
158
159
160
    /**
161
     * Execute the Update with PDO
162
     *
163
     * @param  string $primaryKey
164
     * @param  string $primaryKeyVal  Primary Key's Value
165
     * @return string                 Doctrine DBAL QueryBuilder
166
     */
167 9
    private function prepareUpdate(string $primaryKey, $primaryKeyVal): QueryBuilder
168
    {
169 9
        $data = $this->generateFakeData();
170
171 7
        $queryBuilder = $this->conn->createQueryBuilder();
172 7
        $queryBuilder = $queryBuilder->update($this->entity);
173 7
        foreach ($data as $field => $value) {
174 7
            $queryBuilder = $queryBuilder->set($field, $this->getCondition($field));
175 7
            $queryBuilder = $queryBuilder->setParameter(":$field", $value);
176
        }
177 7
        $queryBuilder = $queryBuilder->where("$primaryKey = :$primaryKey");
178 7
        $queryBuilder = $queryBuilder->setParameter(":$primaryKey", $primaryKeyVal);
179
180 7
        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 6
    private function getRawSQL(QueryBuilder $queryBuilder)
190
    {
191 6
        $sql = $queryBuilder->getSQL();
192 6
        foreach ($queryBuilder->getParameters() as $parameter => $value) {
193 6
            $sql = str_replace($parameter, "'$value'", $sql);
194
        }
195
196 6
        return $sql;
197
    }
198
199
200
    /**
201
     * Execute the Delete with PDO
202
     *
203
     * @param string $where
204
     * @param bool   $pretend
205
     *
206
     * @return string
207
     */
208 4
    private function runDelete(string $where, bool $pretend): string
209
    {
210 4
        $queryBuilder = $this->conn->createQueryBuilder();
211 4
        $queryBuilder = $queryBuilder->delete($this->entity);
212 4
        if (!empty($where)) {
213 3
            $queryBuilder = $queryBuilder->where($where);
214
        }
215 4
        $sql = $queryBuilder->getSQL();
216
217 4
        if ($pretend === true) {
218 1
            return $sql;
219
        }
220
221
        try {
222 3
            $queryBuilder->execute();
223 1
        } catch (\Exception $e) {
224 1
            throw new NeuralizerException('Query DELETE Error (' . $e->getMessage() . ')');
225
        }
226
227 2
        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 7
    private function getCondition(string $field): string
238
    {
239 7
        $type = strtolower($this->entityCols[$field]['type']);
240
241 7
        $integerCast = $this->getIntegerCast($field);
242
243 7
        $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
244
245
        $typeToCast = [
246 7
            'date'     => 'DATE',
247 7
            'datetime' => 'DATE',
248 7
            'time'     => 'TIME',
249 7
            'smallint' => $integerCast,
250 7
            'integer'  => $integerCast,
251 7
            'bigint'   => $integerCast,
252 7
            'float'    => 'DECIMAL',
253 7
            'decimal'  => 'DECIMAL',
254
255
        ];
256
257
        // No cast required
258 7
        if (!array_key_exists($type, $typeToCast)) {
259 7
            return $condition;
260
        }
261
262 7
        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 7
    private function getIntegerCast(string $field): string
273
    {
274 7
        $driver = $this->getConn()->getDriver();
275 7
        if ($driver->getName() === 'pdo_mysql') {
276 7
            return $this->entityCols[$field]['unsigned'] === true ? 'UNSIGNED' : 'SIGNED';
277
        }
278
279
        return 'INTEGER';
280
    }
281
}
282