Issues (590)

src/Connection/SimpleConnection.php (3 issues)

1
<?php
2
3
namespace Bdf\Prime\Connection;
4
5
use Bdf\Prime\Connection\Event\ConnectionClosedListenerInterface;
6
use Bdf\Prime\Connection\Extensions\LostConnection;
7
use Bdf\Prime\Connection\Extensions\SchemaChanged;
8
use Bdf\Prime\Connection\Result\DoctrineResultSet;
9
use Bdf\Prime\Connection\Result\ResultSetInterface;
10
use Bdf\Prime\Connection\Result\UpdateResultSet;
11
use Bdf\Prime\Exception\DBALException;
12
use Bdf\Prime\Exception\PrimeException;
13
use Bdf\Prime\Exception\QueryExecutionException;
14
use Bdf\Prime\Platform\PlatformInterface;
15
use Bdf\Prime\Platform\Sql\SqlPlatform;
16
use Bdf\Prime\Query\CommandInterface;
17
use Bdf\Prime\Query\Compiler\Preprocessor\PreprocessorInterface;
18
use Bdf\Prime\Query\Compiler\SqlCompiler;
19
use Bdf\Prime\Query\Contract\Compilable;
20
use Bdf\Prime\Query\Contract\Query\InsertQueryInterface;
21
use Bdf\Prime\Query\Contract\Query\KeyValueQueryInterface;
22
use Bdf\Prime\Query\Custom\BulkInsert\BulkInsertQuery;
23
use Bdf\Prime\Query\Custom\BulkInsert\BulkInsertSqlCompiler;
24
use Bdf\Prime\Query\Custom\KeyValue\KeyValueQuery;
25
use Bdf\Prime\Query\Custom\KeyValue\KeyValueSqlCompiler;
26
use Bdf\Prime\Query\Factory\DefaultQueryFactory;
27
use Bdf\Prime\Query\Factory\QueryFactoryInterface;
28
use Bdf\Prime\Query\Query;
29
use Bdf\Prime\Query\ReadCommandInterface;
30
use Bdf\Prime\Schema\SchemaManager;
31
use Closure;
32
use Doctrine\Common\EventManager;
33
use Doctrine\DBAL\Cache\QueryCacheProfile;
34
use Doctrine\DBAL\Configuration;
35
use Doctrine\DBAL\Connection as BaseConnection;
36
use Doctrine\DBAL\Driver;
37
use Doctrine\DBAL\Exception as DoctrineDBALException;
38
use Doctrine\DBAL\Exception\DriverException;
39
use Doctrine\DBAL\Result;
40
use Doctrine\DBAL\Statement;
41
42
/**
43
 * Connection
44
 *
45
 * @method \Bdf\Prime\Configuration getConfiguration()
46
 */
47
class SimpleConnection extends BaseConnection implements ConnectionInterface, TransactionManagerInterface
48
{
49
    use LostConnection;
50
    use SchemaChanged;
51
52
    /**
53
     * The connection name.
54
     *
55
     * @var string
56
     */
57
    protected $name;
58
59
    /**
60
     * The schema manager.
61
     *
62
     * @var SchemaManager
63
     */
64
    private $schema;
65
66
    /**
67
     * @var SqlPlatform
68
     */
69
    private $platform;
70
71
    /**
72
     * @var QueryFactoryInterface
73
     */
74
    private $factory;
75
76
    /**
77
     * SimpleConnection constructor.
78
     *
79
     * @param array $params
80
     * @param Driver $driver
81
     * @param Configuration|null $config
82
     * @param EventManager|null $eventManager
83
     * @throws DoctrineDBALException
84
     */
85 402
    public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $eventManager = null)
86
    {
87
        /** @psalm-suppress InternalMethod */
88 402
        parent::__construct($params, $driver, $config, $eventManager);
89
90
        /** @psalm-suppress InvalidArgument */
91 402
        $this->factory = new DefaultQueryFactory(
92 402
            $this,
93 402
            new SqlCompiler($this),
94 402
            [
95 402
                KeyValueQuery::class   => KeyValueSqlCompiler::class,
96 402
                BulkInsertQuery::class => BulkInsertSqlCompiler::class,
97 402
            ],
98 402
            [
99 402
                KeyValueQueryInterface::class => KeyValueQuery::class,
100 402
                InsertQueryInterface::class   => BulkInsertQuery::class,
101 402
            ]
102 402
        );
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 403
    public function setName(string $name)
109
    {
110 403
        $this->name = $name;
111
112 403
        return $this;
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 406
    public function getName(): string
119
    {
120 406
        return $this->name;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 1015
    public function getDatabase(): ?string
127
    {
128 1015
        return parent::getDatabase();
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 1
    public function isConnected()
135
    {
136 1
        return $this->_conn !== null;
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 1087
    public function schema(): SchemaManager
143
    {
144 1087
        if ($this->schema === null) {
145 368
            $this->schema = new SchemaManager($this);
146
        }
147
148 1087
        return $this->schema;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 1324
    public function platform(): PlatformInterface
155
    {
156 1324
        if ($this->platform === null) {
157
            try {
158 385
                $this->platform = new SqlPlatform($this->getDatabasePlatform(), $this->getConfiguration()->getTypes());
159
            } catch (DoctrineDBALException $e) {
160
                /** @psalm-suppress InvalidScalarArgument */
161
                throw new DBALException($e->getMessage(), $e->getCode(), $e);
162
            }
163
        }
164
165 1324
        return $this->platform;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171 110
    public function fromDatabase($value, $type, array $fieldOptions = [])
172
    {
173 110
        return $this->platform()->types()->fromDatabase($value, $type, $fieldOptions);
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 4
    public function toDatabase($value, $type = null)
180
    {
181 4
        return $this->platform()->types()->toDatabase($value, $type);
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 912
    public function builder(PreprocessorInterface $preprocessor = null): Query
188
    {
189 912
        return $this->factory->make(Query::class, $preprocessor);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->factory->m...::class, $preprocessor) returns the type Bdf\Prime\Query\CommandInterface which includes types incompatible with the type-hinted return Bdf\Prime\Query\Query.
Loading history...
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 439
    public function make(string $query, PreprocessorInterface $preprocessor = null): CommandInterface
196
    {
197 439
        return $this->factory->make($query, $preprocessor);
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 1081
    public function factory(): QueryFactoryInterface
204
    {
205 1081
        return $this->factory;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 729
    public function from($table, ?string $alias = null): Query
212
    {
213 729
        return $this->builder()->from($table, $alias);
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 13
    public function delete($table, array $criteria, array $types = [])
220
    {
221 13
        return $this->from($table)->where($criteria)->delete();
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 1
    public function update($table, array $data, array $criteria, array $types = [])
228
    {
229 1
        return $this->from($table)->where($criteria)->update($data, $types);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 619
    public function insert($table, array $data, array $types = [])
236
    {
237 619
        return $this->from($table)->insert($data);
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 2
    public function select($query, array $bindings = []): ResultSetInterface
244
    {
245 2
        return (new DoctrineResultSet($this->executeQuery($query, $bindings)))->asObject();
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 1114
    public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result
252
    {
253 1114
        $this->prepareLogger();
254
255 1114
        return $this->runOrReconnect(fn () => parent::executeQuery($sql, $params, $types, $qcp));
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 1095
    public function executeStatement($sql, array $params = [], array $types = [])
262
    {
263 1095
        $this->prepareLogger();
264
265 1095
        return $this->runOrReconnect(fn () => parent::executeStatement($sql, $params, $types));
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     *
271
     * @throws PrimeException
272
     */
273 519
    public function prepare(string $sql): Statement
274
    {
275 519
        return $this->runOrReconnect(fn () => parent::prepare($sql));
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281 863
    public function execute(Compilable $query): ResultSetInterface
282
    {
283
        try {
284 863
            $statement = $query->compile();
285
286 860
            if ($statement instanceof Statement) {
287 715
                return $this->executePrepared($statement, $query);
288
            }
289
290
            // $statement is a SQL query
291 798
            if ($query->type() === Compilable::TYPE_SELECT) {
292 741
                return new DoctrineResultSet($this->executeQuery($statement, $query->getBindings()));
293
            }
294
295 674
            return new UpdateResultSet((int) $this->executeStatement($statement, $query->getBindings()));
296 13
        } catch (DriverException $e) {
297 8
            throw new QueryExecutionException(
298 8
                'Error on execute : ' . $e->getMessage(),
299 8
                $e->getCode(),
300 8
                $e,
301 8
                $e->getQuery() ? $e->getQuery()->getSQL() : null,
302 8
                $e->getQuery() ? $e->getQuery()->getParams() : null
303 8
            );
304 5
        } catch (DoctrineDBALException $e) {
305
            /** @psalm-suppress InvalidScalarArgument */
306
            throw new QueryExecutionException('Error on execute : '.$e->getMessage(), $e->getCode(), $e);
307
        }
308
    }
309
310
    /**
311
     * Execute a prepared statement
312
     *
313
     * @param Statement $statement
314
     * @param Compilable $query
315
     *
316
     * @return ResultSetInterface The query result
317
     *
318
     * @throws DoctrineDBALException
319
     * @throws PrimeException
320
     *
321
     * @psalm-suppress InternalMethod
322
     */
323 715
    protected function executePrepared(Statement $statement, Compilable $query)
324
    {
325 715
        $bindings = $query->getBindings();
326 715
        $isRead = $query->type() === Compilable::TYPE_SELECT;
327
328 715
        $this->prepareLogger();
329
330
        try {
331 715
            $result = $isRead
332 309
                ? new DoctrineResultSet($statement->executeQuery($bindings))
333 715
                : new UpdateResultSet($statement->executeStatement($bindings))
334 715
            ;
335 9
        } catch (DoctrineDBALException $exception) {
336
            // Prepared query on SQLite for PHP < 7.2 invalidates the query when schema change
337
            // This process may be removed on PHP 7.2
338 9
            if ($this->causedBySchemaChange($exception)) {
339 2
                $statement = $query->compile(true);
340 2
                $result = $isRead
341
                    ? new DoctrineResultSet($statement->executeQuery($query->getBindings()))
342 2
                    : new UpdateResultSet($statement->executeStatement($query->getBindings()))
343 2
                ;
344 7
            } elseif ($this->causedByLostConnection($exception->getPrevious())) { // If the connection is lost, the query must be recompiled
345
                $this->close();
346
                $this->connect();
347
348
                $statement = $query->compile(true);
349
                $result = $isRead
350
                    ? new DoctrineResultSet($statement->executeQuery($query->getBindings()))
351
                    : new UpdateResultSet($statement->executeStatement($query->getBindings()))
352
                ;
353
            } else {
354 7
                throw $exception;
355
            }
356
        }
357
358 713
        return $result;
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364 1101
    public function beginTransaction(): bool
365
    {
366 1101
        $this->prepareLogger();
367
368 1101
        return parent::beginTransaction() ?? true;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374 57
    public function commit(): bool
375
    {
376 57
        $this->prepareLogger();
377
378 57
        return parent::commit() ?? true;
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384 1076
    public function rollBack(): bool
385
    {
386 1076
        $this->prepareLogger();
387
388 1076
        return parent::rollBack() ?? true;
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394 16
    public function close(): void
395
    {
396 16
        parent::close();
397
398 16
        $this->_eventManager->dispatchEvent(ConnectionClosedListenerInterface::EVENT_NAME);
0 ignored issues
show
Deprecated Code introduced by
The property Doctrine\DBAL\Connection::$_eventManager has been deprecated. ( Ignorable by Annotation )

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

398
        /** @scrutinizer ignore-deprecated */ $this->_eventManager->dispatchEvent(ConnectionClosedListenerInterface::EVENT_NAME);
Loading history...
399
    }
400
401
    /**
402
     * Setup the logger by setting the connection
403
     *
404
     * @return void
405
     */
406 1252
    protected function prepareLogger(): void
407
    {
408
        /** @psalm-suppress InternalMethod */
409 1252
        $logger = $this->getConfiguration()->getSQLLogger();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Configuration::getSQLLogger() has been deprecated. ( Ignorable by Annotation )

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

409
        $logger = /** @scrutinizer ignore-deprecated */ $this->getConfiguration()->getSQLLogger();
Loading history...
410
411 1252
        if ($logger && $logger instanceof ConnectionAwareInterface) {
412
            $logger->setConnection($this);
413
        }
414
    }
415
416
    /**
417
     * Execute a query. Try to reconnect if needed
418
     *
419
     * @param Closure():T $callback
420
     *
421
     * @return T The query result
422
     *
423
     * @throws QueryExecutionException When an error occurs during query execution
424
     * @throws DBALException When any other error occurs
425
     *
426
     * @template T
427
     *
428
     * @psalm-suppress InternalMethod
429
     */
430 1116
    protected function runOrReconnect(Closure $callback)
431
    {
432
        try {
433
            try {
434 1116
                return $callback();
435 69
            } catch (DoctrineDBALException $exception) {
436 69
                if ($this->causedByLostConnection($exception->getPrevious())) {
437
                    // Should check for active transaction.
438
                    // Only reconnect the start transaction.
439
                    // Should raise exception during transaction.
440 2
                    $this->close();
441
                    /** @psalm-suppress InternalMethod */
442 2
                    $this->connect();
443
444 2
                    return $callback();
445
                }
446
447 67
                throw $exception;
448
            }
449 67
        } catch (DriverException $e) {
450 67
            throw new QueryExecutionException(
451 67
                'Error on execute : ' . $e->getMessage(),
452 67
                $e->getCode(),
453 67
                $e,
454 67
                $e->getQuery() ? $e->getQuery()->getSQL() : null,
455 67
                $e->getQuery() ? $e->getQuery()->getParams() : null
456 67
            );
457
        } catch (DoctrineDBALException $e) {
458
            /** @psalm-suppress InvalidScalarArgument */
459
            throw new DBALException('Error on execute : '.$e->getMessage(), $e->getCode(), $e);
460
        }
461
    }
462
}
463