Passed
Pull Request — 2.0 (#53)
by Vincent
13:12 queued 06:53
created

SimpleConnection::make()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 400
    public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $eventManager = null)
86
    {
87
        /** @psalm-suppress InternalMethod */
88 400
        parent::__construct($params, $driver, $config, $eventManager);
89
90
        /** @psalm-suppress InvalidArgument */
91 400
        $this->factory = new DefaultQueryFactory(
92
            $this,
93 400
            new SqlCompiler($this),
94
            [
95 400
                KeyValueQuery::class   => KeyValueSqlCompiler::class,
96
                BulkInsertQuery::class => BulkInsertSqlCompiler::class,
97
            ],
98
            [
99 400
                KeyValueQueryInterface::class => KeyValueQuery::class,
100
                InsertQueryInterface::class   => BulkInsertQuery::class,
101
            ]
102
        );
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 401
    public function setName(string $name)
109
    {
110 401
        $this->name = $name;
111
112 401
        return $this;
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 404
    public function getName(): string
119
    {
120 404
        return $this->name;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 996
    public function getDatabase(): ?string
127
    {
128 996
        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 1082
    public function schema(): SchemaManager
143
    {
144 1082
        if ($this->schema === null) {
145 366
            $this->schema = new SchemaManager($this);
146
        }
147
148 1082
        return $this->schema;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 1316
    public function platform(): PlatformInterface
155
    {
156 1316
        if ($this->platform === null) {
157
            try {
158 383
                $config = $this->getConfiguration();
159 383
                $this->platform = new SqlPlatform($this->getDatabasePlatform(), $config->getTypes());
160 383
                $types = $this->platform->types();
161
162 383
                foreach ($config->getPlatformTypes() as $alias => $type) {
163 1
                    $types->register($type, is_string($alias) ? $alias : null);
164
                }
165
            } catch (DoctrineDBALException $e) {
166
                /** @psalm-suppress InvalidScalarArgument */
167
                throw new DBALException($e->getMessage(), $e->getCode(), $e);
168
            }
169
        }
170
171 1316
        return $this->platform;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 108
    public function fromDatabase($value, $type, array $fieldOptions = [])
178
    {
179 108
        return $this->platform()->types()->fromDatabase($value, $type, $fieldOptions);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 5
    public function toDatabase($value, $type = null)
186
    {
187 5
        return $this->platform()->types()->toDatabase($value, $type);
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 907
    public function builder(PreprocessorInterface $preprocessor = null): Query
194
    {
195 907
        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...
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 420
    public function make(string $query, PreprocessorInterface $preprocessor = null): CommandInterface
202
    {
203 420
        return $this->factory->make($query, $preprocessor);
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209 1075
    public function factory(): QueryFactoryInterface
210
    {
211 1075
        return $this->factory;
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 727
    public function from($table, ?string $alias = null): Query
218
    {
219 727
        return $this->builder()->from($table, $alias);
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 13
    public function delete($table, array $criteria, array $types = [])
226
    {
227 13
        return $this->from($table)->where($criteria)->delete();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 1
    public function update($table, array $data, array $criteria, array $types = [])
234
    {
235 1
        return $this->from($table)->where($criteria)->update($data, $types);
236
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 618
    public function insert($table, array $data, array $types = [])
242
    {
243 618
        return $this->from($table)->insert($data);
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 2
    public function select($query, array $bindings = []): ResultSetInterface
250
    {
251 2
        return (new DoctrineResultSet($this->executeQuery($query, $bindings)))->asObject();
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 1109
    public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result
258
    {
259 1109
        $this->prepareLogger();
260
261 1109
        return $this->runOrReconnect(fn () => parent::executeQuery($sql, $params, $types, $qcp));
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 1090
    public function executeStatement($sql, array $params = [], array $types = [])
268
    {
269 1090
        $this->prepareLogger();
270
271 1090
        return $this->runOrReconnect(fn () => parent::executeStatement($sql, $params, $types));
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     *
277
     * @throws PrimeException
278
     */
279 501
    public function prepare(string $sql): Statement
280
    {
281 501
        return $this->runOrReconnect(fn () => parent::prepare($sql));
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287 859
    public function execute(Compilable $query): ResultSetInterface
288
    {
289
        try {
290 859
            $statement = $query->compile();
291
292 856
            if ($statement instanceof Statement) {
293 711
                return $this->executePrepared($statement, $query);
294
            }
295
296
            // $statement is a SQL query
297 794
            if ($query->type() === Compilable::TYPE_SELECT) {
298 737
                return new DoctrineResultSet($this->executeQuery($statement, $query->getBindings()));
299
            }
300
301 673
            return new UpdateResultSet((int) $this->executeStatement($statement, $query->getBindings()));
302 13
        } catch (DriverException $e) {
303 8
            throw new QueryExecutionException(
304 8
                'Error on execute : ' . $e->getMessage(),
305 8
                $e->getCode(),
306
                $e,
307 8
                $e->getQuery() ? $e->getQuery()->getSQL() : null,
308 8
                $e->getQuery() ? $e->getQuery()->getParams() : null
309
            );
310 5
        } catch (DoctrineDBALException $e) {
311
            /** @psalm-suppress InvalidScalarArgument */
312
            throw new QueryExecutionException('Error on execute : '.$e->getMessage(), $e->getCode(), $e);
313
        }
314
    }
315
316
    /**
317
     * Execute a prepared statement
318
     *
319
     * @param Statement $statement
320
     * @param Compilable $query
321
     *
322
     * @return ResultSetInterface The query result
323
     *
324
     * @throws DoctrineDBALException
325
     * @throws PrimeException
326
     *
327
     * @psalm-suppress InternalMethod
328
     */
329 711
    protected function executePrepared(Statement $statement, Compilable $query)
330
    {
331 711
        $bindings = $query->getBindings();
332 711
        $isRead = $query->type() === Compilable::TYPE_SELECT;
333
334 711
        $this->prepareLogger();
335
336
        try {
337 711
            $result = $isRead
338 309
                ? new DoctrineResultSet($statement->executeQuery($bindings))
339 711
                : new UpdateResultSet($statement->executeStatement($bindings))
340
            ;
341 9
        } catch (DoctrineDBALException $exception) {
342
            // Prepared query on SQLite for PHP < 7.2 invalidates the query when schema change
343
            // This process may be removed on PHP 7.2
344 9
            if ($this->causedBySchemaChange($exception)) {
345 2
                $statement = $query->compile(true);
346 2
                $result = $isRead
347
                    ? new DoctrineResultSet($statement->executeQuery($query->getBindings()))
348 2
                    : new UpdateResultSet($statement->executeStatement($query->getBindings()))
349
                ;
350 7
            } elseif ($this->causedByLostConnection($exception->getPrevious())) { // If the connection is lost, the query must be recompiled
351
                $this->close();
352
                $this->connect();
353
354
                $statement = $query->compile(true);
355
                $result = $isRead
356
                    ? new DoctrineResultSet($statement->executeQuery($query->getBindings()))
357
                    : new UpdateResultSet($statement->executeStatement($query->getBindings()))
358
                ;
359
            } else {
360 7
                throw $exception;
361
            }
362
        }
363
364 709
        return $result;
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     */
370 1099
    public function beginTransaction(): bool
371
    {
372 1099
        $this->prepareLogger();
373
374 1099
        return parent::beginTransaction() ?? true;
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     */
380 57
    public function commit(): bool
381
    {
382 57
        $this->prepareLogger();
383
384 57
        return parent::commit() ?? true;
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     */
390 1073
    public function rollBack(): bool
391
    {
392 1073
        $this->prepareLogger();
393
394 1073
        return parent::rollBack() ?? true;
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     */
400 16
    public function close(): void
401
    {
402 16
        parent::close();
403
404 16
        $this->_eventManager->dispatchEvent(ConnectionClosedListenerInterface::EVENT_NAME);
405
    }
406
407
    /**
408
     * Setup the logger by setting the connection
409
     *
410
     * @return void
411
     */
412 1247
    protected function prepareLogger(): void
413
    {
414
        /** @psalm-suppress InternalMethod */
415 1247
        $logger = $this->getConfiguration()->getSQLLogger();
416
417 1247
        if ($logger && $logger instanceof ConnectionAwareInterface) {
418
            $logger->setConnection($this);
419
        }
420
    }
421
422
    /**
423
     * Execute a query. Try to reconnect if needed
424
     *
425
     * @param Closure():T $callback
426
     *
427
     * @return T The query result
428
     *
429
     * @throws QueryExecutionException When an error occurs during query execution
430
     * @throws DBALException When any other error occurs
431
     *
432
     * @template T
433
     *
434
     * @psalm-suppress InternalMethod
435
     */
436 1111
    protected function runOrReconnect(Closure $callback)
437
    {
438
        try {
439
            try {
440 1111
                return $callback();
441 70
            } catch (DoctrineDBALException $exception) {
442 70
                if ($this->causedByLostConnection($exception->getPrevious())) {
443
                    // Should check for active transaction.
444
                    // Only reconnect the start transaction.
445
                    // Should raise exception during transaction.
446 2
                    $this->close();
447
                    /** @psalm-suppress InternalMethod */
448 2
                    $this->connect();
449
450 2
                    return $callback();
451
                }
452
453 68
                throw $exception;
454
            }
455 68
        } catch (DriverException $e) {
456 68
            throw new QueryExecutionException(
457 68
                'Error on execute : ' . $e->getMessage(),
458 68
                $e->getCode(),
459
                $e,
460 68
                $e->getQuery() ? $e->getQuery()->getSQL() : null,
461 68
                $e->getQuery() ? $e->getQuery()->getParams() : null
462
            );
463
        } catch (DoctrineDBALException $e) {
464
            /** @psalm-suppress InvalidScalarArgument */
465
            throw new DBALException('Error on execute : '.$e->getMessage(), $e->getCode(), $e);
466
        }
467
    }
468
}
469