Passed
Push — master ( c103e2...560387 )
by Emmanuel
03:39
created

DB::runUpdate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.5

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 3
cts 6
cp 0.5
rs 9.4285
c 0
b 0
f 0
nc 2
cc 2
eloc 6
nop 1
crap 2.5
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 29
    public function __construct(array $params)
44
    {
45 29
        $this->conn = DbalDriverManager::getConnection($params, new DbalConfiguration());
46 28
    }
47
48
49
    /**
50
     * Get Doctrine Connection
51
     * @return Doctrine\DBAL\Connection
52
     */
53 17
    public function getConn()
54
    {
55 17
        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 13
    public function processEntity(
70
        string $entity,
71
        callable $callback = null,
72
        bool $pretend = true,
73
        bool $returnRes = false
74
    ): array {
75 13
        $schema = $this->conn->getSchemaManager();
76 13
        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 12
        $this->entity = $entity;
81 12
        $queries = [];
82
83 12
        $actionsOnThatEntity = $this->whatToDoWithEntity();
84
85 10
        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 9
        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 8
            $rowNum = 0;
95
96 8
            $key = $this->getPrimaryKey();
97 7
            $this->entityCols = $this->getTableCols();
98
99 7
            $queryBuilder = $this->conn->createQueryBuilder();
100 7
            $rows = $queryBuilder->select($key)->from($this->entity)->execute();
101
102 7
            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 7
                $data = $this->generateFakeData();
104 7
                $queryBuilder = $this->prepareUpdate($data, $key, $row[$key]);
105
106 7
                ($returnRes === true ? array_push($queries, $this->getRawSQL($queryBuilder)) : '');
107
108 7
                if ($pretend === false) {
109 4
                    $this->runUpdate($queryBuilder);
110
                }
111
112 7
                if (!is_null($callback)) {
113 7
                    $callback(++$rowNum);
114
                }
115
            }
116
        }
117
118 8
        return $queries;
119
    }
120
121
122
    /**
123
     * Identify the primary key for a table
124
     *
125
     * @return string Field's name
126
     */
127 8
    private function getPrimaryKey()
128
    {
129 8
        $schema = $this->conn->getSchemaManager();
130 8
        $tableDetails = $schema->listTableDetails($this->entity);
131 8
        if ($tableDetails->hasPrimaryKey() === false) {
132 1
            throw new NeuralizerException("Can't find a primary key for '{$this->entity}'");
133
        }
134
135 7
        return $tableDetails->getPrimaryKey()->getColumns()[0];
136
    }
137
138
139
    /**
140
     * Retrieve columns list for a table with type and length
141
     *
142
     * @return array $cols
143
     */
144 7
    private function getTableCols()
145
    {
146 7
        $schema = $this->conn->getSchemaManager();
147 7
        $tableCols = $schema->listTableColumns($this->entity);
148 7
        $cols = [];
149 7
        foreach ($tableCols as $col) {
150 7
            $cols[$col->getName()] = [
151 7
                'length' => $col->getLength(),
152 7
                'type'   => $col->getType(),
153
            ];
154
        }
155
156 7
        return $cols;
157
    }
158
159
160
    /**
161
     * Execute the Update with PDO
162
     *
163
     * @param  array  $data           Array of fields => value to update the table
164
     * @param  string $primaryKey
165
     * @param  string $primaryKeyVal  Primary Key's Value
166
     * @return string                 Doctrine DBAL QueryBuilder
167
     */
168 7
    private function prepareUpdate(array $data, string $primaryKey, $primaryKeyVal)
169
    {
170 7
        $queryBuilder = $this->conn->createQueryBuilder();
171 7
        $queryBuilder = $queryBuilder->update($this->entity);
172 7
        foreach ($data as $field => $value) {
173 7
            $type = $this->entityCols[$field]['type'];
174 7
            $condition = "(CASE $field WHEN NULL THEN NULL ELSE :$field END)";
175 7
            $queryBuilder = $queryBuilder->set($field, $condition);
176 7
            $queryBuilder = $queryBuilder->setParameter(":$field", $value, $type);
177
        }
178 7
        $queryBuilder = $queryBuilder->where("$primaryKey = :$primaryKey");
179 7
        $queryBuilder = $queryBuilder->setParameter(":$primaryKey", $primaryKeyVal);
180
181 7
        return $queryBuilder;
182
    }
183
184
185
    /**
186
     * Execute the Update with PDO
187
     *
188
     * @param QueryBuilder $queryBuilder
189
     */
190 4
    private function runUpdate(QueryBuilder $queryBuilder)
191
    {
192
        try {
193 4
            $queryBuilder->execute();
194
        } catch (\Doctrine\DBAL\Exception\DriverException $e) {
195
            throw new NeuralizerException(
196
                "Problem anonymizing {$this->entity} (" . $e->getMessage() . ')'
197
            );
198
        }
199 4
    }
200
201
202
    /**
203
     * To debug, build the final SQL (can be approximative)
204
     * @param  QueryBuilder $queryBuilder
205
     * @return string
206
     */
207 6
    private function getRawSQL(QueryBuilder $queryBuilder)
208
    {
209 6
        $sql = $queryBuilder->getSQL();
210 6
        foreach ($queryBuilder->getParameters() as $parameter => $value) {
211 6
            if (is_object($value)) {
212 6
                $value = '{object}';
213
            }
214 6
            $sql = str_replace($parameter, "'$value'", $sql);
215
        }
216
217 6
        return $sql;
218
    }
219
220
221
    /**
222
     * Execute the Delete with PDO
223
     *
224
     * @param string $where
225
     * @param bool   $pretend
226
     *
227
     * @return string
228
     */
229 4
    private function runDelete(string $where, bool $pretend): string
230
    {
231 4
        $queryBuilder = $this->conn->createQueryBuilder();
232 4
        $queryBuilder = $queryBuilder->delete($this->entity);
233 4
        if (!empty($where)) {
234 3
            $queryBuilder = $queryBuilder->where($where);
235
        }
236 4
        $sql = $queryBuilder->getSQL();
237
238 4
        if ($pretend === true) {
239 1
            return $sql;
240
        }
241
242
        try {
243 3
            $queryBuilder->execute();
244 1
        } catch (\Exception $e) {
245 1
            throw new NeuralizerException('Query DELETE Error (' . $e->getMessage() . ')');
246
        }
247
248 2
        return $sql;
249
    }
250
}
251