Passed
Push — devel-3.0 ( 5647ad...19c8c8 )
by Rubén
04:03
created

Database::setFetchMode()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 2
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2018, Rubén Domínguez nuxsmin@$syspass.org
8
 *
9
 * This file is part of sysPass.
10
 *
11
 * sysPass is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation, either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * sysPass is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 *  along with sysPass.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace SP\Storage\Database;
26
27
use PDO;
28
use PDOStatement;
29
use SP\Core\Events\Event;
30
use SP\Core\Events\EventDispatcher;
31
use SP\Core\Events\EventMessage;
32
use SP\Core\Exceptions\ConstraintException;
33
use SP\Core\Exceptions\QueryException;
34
use SP\Core\Exceptions\SPException;
35
36
/**
37
 * Class Database
38
 *
39
 * @package SP\Storage
40
 */
41
final class Database implements DatabaseInterface
42
{
43
    /**
44
     * @var int Número de registros obtenidos
45
     */
46
    protected $numRows = 0;
47
    /**
48
     * @var int Número de campos de la consulta
49
     */
50
    protected $numFields = 0;
51
    /**
52
     * @var array Resultados de la consulta
53
     */
54
    protected $lastResult;
55
    /**
56
     * @var DBStorageInterface
57
     */
58
    protected $dbHandler;
59
    /**
60
     * @var int Último Id de elemento insertado/actualizado
61
     */
62
    private $lastId;
63
    /**
64
     * @var EventDispatcher
65
     */
66
    private $eventDispatcher;
67
68
    /**
69
     * DB constructor.
70
     *
71
     * @param DBStorageInterface $dbHandler
72
     * @param EventDispatcher    $eventDispatcher
73
     */
74
    public function __construct(DBStorageInterface $dbHandler, EventDispatcher $eventDispatcher)
75
    {
76
        $this->dbHandler = $dbHandler;
77
        $this->eventDispatcher = $eventDispatcher;
78
    }
79
80
    /**
81
     * @return int
82
     */
83
    public function getNumRows()
84
    {
85
        return $this->numRows;
86
    }
87
88
    /**
89
     * @return int
90
     */
91
    public function getNumFields()
92
    {
93
        return $this->numFields;
94
    }
95
96
    /**
97
     * @return array
98
     */
99
    public function getLastResult()
100
    {
101
        return $this->lastResult;
102
    }
103
104
    /**
105
     * @return int
106
     */
107
    public function getLastId()
108
    {
109
        return $this->lastId;
110
    }
111
112
    /**
113
     * @return DBStorageInterface
114
     */
115
    public function getDbHandler()
116
    {
117
        return $this->dbHandler;
118
    }
119
120
    /**
121
     * @param QueryData $queryData
122
     * @param bool      $fullCount
123
     *
124
     * @return QueryResult
125
     * @throws ConstraintException
126
     * @throws QueryException
127
     */
128
    public function doSelect(QueryData $queryData, $fullCount = false)
129
    {
130
        if ($queryData->getQuery() === '') {
131
            throw new QueryException($queryData->getOnErrorMessage(), QueryException::ERROR, __u('Blank query'));
132
        }
133
134
        try {
135
            $queryResult = $this->doQuery($queryData);
136
137
            if ($fullCount === true) {
138
                $queryResult->setTotalNumRows($this->getFullRowCount($queryData));
139
            }
140
141
            return $queryResult;
142
        } catch (ConstraintException $e) {
143
            processException($e);
144
145
            throw $e;
146
        } catch (QueryException $e) {
147
            processException($e);
148
149
            throw $e;
150
        } catch (\Exception $e) {
151
            processException($e);
152
153
            throw new QueryException(
154
                $queryData->getOnErrorMessage(),
155
                SPException::ERROR,
156
                $e->getMessage(),
157
                $e->getCode(),
158
                $e
159
            );
160
        }
161
    }
162
163
    /**
164
     * Realizar una consulta a la BBDD.
165
     *
166
     * @param $queryData   QueryData Los datos de la consulta
167
     * @param $getRawData  bool    realizar la consulta para obtener registro a registro
168
     *
169
     * @return QueryResult
170
     * @throws QueryException
171
     * @throws ConstraintException
172
     */
173
    public function doQuery(QueryData $queryData, $getRawData = false)
174
    {
175
        $stmt = $this->prepareQueryData($queryData);
176
177
        $this->setFetchMode($queryData, $stmt);
178
179
        $this->eventDispatcher->notifyEvent('database.query',
180
            new Event($this, EventMessage::factory()
181
                ->addDescription($queryData->getQuery())
182
            )
183
        );
184
185
        if (preg_match("/^(select|show)\s/i", $queryData->getQuery())) {
186
            $this->numFields = $stmt->columnCount();
187
188
            return new QueryResult($stmt->fetchAll());
189
        }
190
191
        return (new QueryResult())
192
            ->setAffectedNumRows($stmt->rowCount())
193
            ->setLastId($this->lastId);
194
    }
195
196
    /**
197
     * Asociar los parámetros de la consulta utilizando el tipo adecuado
198
     *
199
     * @param QueryData $queryData Los datos de la consulta
200
     * @param bool      $isCount   Indica si es una consulta de contador de registros
201
     * @param array     $options
202
     *
203
     * @return \PDOStatement
204
     * @throws ConstraintException
205
     * @throws QueryException
206
     */
207
    private function prepareQueryData(QueryData $queryData, $isCount = false, array $options = [])
208
    {
209
        $query = $queryData->getQuery();
210
        $params = $queryData->getParams();
211
212
        if ($isCount === true) {
213
            $query = $queryData->getQueryCount();
214
            $params = $this->getParamsForCount($queryData);
215
        }
216
217
        try {
218
            $connection = $this->dbHandler->getConnection();
219
220
            if (!empty($params)) {
221
                $stmt = $connection->prepare($query, $options);
222
223
                foreach ($params as $param => $value) {
224
                    // Si la clave es un número utilizamos marcadores de posición "?" en
225
                    // la consulta. En caso contrario marcadores de nombre
226
                    $param = is_int($param) ? $param + 1 : ':' . $param;
227
228
                    if ($param === 'blobcontent') {
229
                        $stmt->bindValue($param, $value, PDO::PARAM_LOB);
230
                    } elseif (is_int($value)) {
231
                        $stmt->bindValue($param, $value, PDO::PARAM_INT);
232
                    } else {
233
                        $stmt->bindValue($param, $value);
234
                    }
235
                }
236
237
                $stmt->execute();
238
            } else {
239
                $stmt = $connection->query($query);
240
            }
241
242
            $this->lastId = $connection->lastInsertId();
0 ignored issues
show
Documentation Bug introduced by
The property $lastId was declared of type integer, but $connection->lastInsertId() is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
243
244
            return $stmt;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $stmt also could return the type boolean which is incompatible with the documented return type PDOStatement.
Loading history...
245
        } catch (\Exception $e) {
246
            processException($e);
247
248
            switch ((int)$e->getCode()) {
249
                case 23000:
250
                    throw new ConstraintException(
251
                        __u('Integrity constraint'),
252
                        ConstraintException::ERROR,
253
                        $e->getMessage(),
254
                        $e->getCode(),
255
                        $e
256
                    );
257
            }
258
259
            throw new QueryException($e->getMessage(), QueryException::CRITICAL, $e->getCode(), 0, $e);
260
        }
261
    }
262
263
    /**
264
     * Strips out the unused params from the query count
265
     *
266
     * @param QueryData $queryData
267
     *
268
     * @return array
269
     */
270
    private function getParamsForCount(QueryData $queryData)
271
    {
272
        $countSelect = substr_count($queryData->getSelect(), '?');
273
        $countFrom = substr_count($queryData->getFrom(), '?');
274
        $countWhere = substr_count($queryData->getWhere(), '?');
275
276
        return array_slice($queryData->getParams(), $countSelect, $countFrom + $countWhere);
277
    }
278
279
    /**
280
     * @param QueryData    $queryData
281
     * @param PDOStatement $stmt
282
     */
283
    private function setFetchMode(QueryData $queryData, PDOStatement $stmt)
284
    {
285
        if ($queryData->isUseKeyPair()) {
286
            $stmt->setFetchMode(PDO::FETCH_KEY_PAIR);
287
        } elseif (null !== $queryData->getMapClass()) {
288
            $stmt->setFetchMode(PDO::FETCH_INTO, $queryData->getMapClass());
289
        } elseif ($queryData->getMapClassName()) {
290
            $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, $queryData->getMapClassName());
0 ignored issues
show
Bug introduced by
$queryData->getMapClassName() of type string is incompatible with the type integer expected by parameter $columnNumber of PDOStatement::setFetchMode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

290
            $stmt->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, /** @scrutinizer ignore-type */ $queryData->getMapClassName());
Loading history...
291
        } else {
292
            $stmt->setFetchMode(PDO::FETCH_OBJ);
293
        }
294
    }
295
296
    /**
297
     * Obtener el número de filas de una consulta realizada
298
     *
299
     * @param $queryData QueryData Los datos de la consulta
300
     *
301
     * @return int Número de files de la consulta
302
     * @throws SPException
303
     */
304
    public function getFullRowCount(QueryData $queryData)
305
    {
306
        if ($queryData->getQueryCount() === '') {
307
            return 0;
308
        }
309
310
        $queryRes = $this->prepareQueryData($queryData, true);
311
        $num = (int)$queryRes->fetchColumn();
312
        $queryRes->closeCursor();
313
314
        return $num;
315
    }
316
317
    /**
318
     * Don't fetch records and return prepared statement
319
     *
320
     * @param QueryData  $queryData
321
     * @param array|null $options
322
     *
323
     * @return \PDOStatement
324
     * @throws ConstraintException
325
     * @throws QueryException
326
     */
327
    public function doQueryRaw(QueryData $queryData, array $options = [])
328
    {
329
        return $this->prepareQueryData($queryData, $options);
0 ignored issues
show
Bug introduced by
$options of type array is incompatible with the type boolean expected by parameter $isCount of SP\Storage\Database\Database::prepareQueryData(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

329
        return $this->prepareQueryData($queryData, /** @scrutinizer ignore-type */ $options);
Loading history...
330
    }
331
332
    /**
333
     * Iniciar una transacción
334
     *
335
     * @return bool
336
     */
337
    public function beginTransaction()
338
    {
339
        $conn = $this->dbHandler->getConnection();
340
341
        if (!$conn->inTransaction()) {
342
            $result = $conn->beginTransaction();
343
344
            $this->eventDispatcher->notifyEvent('database.transaction.begin',
345
                new Event($this, EventMessage::factory()->addData('result', $result)));
346
347
            return $result;
348
        } else {
349
            logger('beginTransaction: already in transaction');
350
351
            return true;
352
        }
353
    }
354
355
    /**
356
     * Finalizar una transacción
357
     *
358
     * @return bool
359
     */
360
    public function endTransaction()
361
    {
362
        $conn = $this->dbHandler->getConnection();
363
364
        $result = $conn->inTransaction() && $conn->commit();
365
366
        $this->eventDispatcher->notifyEvent('database.transaction.end',
367
            new Event($this, EventMessage::factory()->addData('result', $result)));
368
369
        return $result;
370
    }
371
372
    /**
373
     * Rollback de una transacción
374
     *
375
     * @return bool
376
     */
377
    public function rollbackTransaction()
378
    {
379
        $conn = $this->dbHandler->getConnection();
380
381
        $result = $conn->inTransaction() && $conn->rollBack();
382
383
        $this->eventDispatcher->notifyEvent('database.transaction.rollback',
384
            new Event($this, EventMessage::factory()->addData('result', $result)));
385
386
        return $result;
387
    }
388
}