Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.70% covered (warning)
69.70%
23 / 33
50.00% covered (danger)
50.00%
6 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReconnectConnection
69.70% covered (warning)
69.70%
23 / 33
50.00% covered (danger)
50.00%
6 / 12
37.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
16.67% covered (danger)
16.67%
1 / 6
0.00% covered (danger)
0.00%
0 / 1
8.21
 query
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 quote
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exec
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 lastInsertId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 beginTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 commit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rollBack
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServerVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 reconnect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDisconnectException
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Satag\AmicronEntityBundle\Doctrine\Middleware;
6
7use Doctrine\DBAL\Driver;
8use Doctrine\DBAL\Driver\Connection;
9use Doctrine\DBAL\Driver\Result;
10use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
11use Doctrine\DBAL\Driver\Statement;
12use Doctrine\DBAL\ParameterType;
13
14/**
15 * Wraps a DBAL driver connection and retries queries once after reconnecting
16 * when a Firebird idle-timeout disconnect is detected.
17 *
18 * Firebird terminates idle connections after ConnectionTimeout (default ~180 s).
19 * The PHP ext-firebird then throws runtime errors on subsequent queries.
20 * This wrapper catches known disconnect errors, reconnects, and retries once.
21 *
22 * Uses a mutable inner connection reference to support reconnection since
23 * AbstractConnectionMiddleware uses an inaccessible private field.
24 */
25final class ReconnectConnection implements Connection, ServerInfoAwareConnection
26{
27    /**
28     * @param array<string, mixed> $params DBAL connection parameters
29     */
30    public function __construct(
31        private Connection $wrapped,
32        private readonly Driver $driver,
33        #[\SensitiveParameter]
34        private readonly array $params
35    ) {
36    }
37
38    public function prepare(string $sql): Statement
39    {
40        try {
41            return $this->wrapped->prepare($sql);
42        } catch (\Throwable $e) {
43            if (!$this->isDisconnectException($e)) {
44                throw $e;
45            }
46
47            $this->reconnect();
48
49            return $this->wrapped->prepare($sql);
50        }
51    }
52
53    public function query(string $sql): Result
54    {
55        try {
56            return $this->wrapped->query($sql);
57        } catch (\Throwable $e) {
58            if (!$this->isDisconnectException($e)) {
59                throw $e;
60            }
61
62            $this->reconnect();
63
64            return $this->wrapped->query($sql);
65        }
66    }
67
68    /**
69     * @param mixed $value
70     */
71    public function quote($value, $type = ParameterType::STRING): mixed
72    {
73        return $this->wrapped->quote($value, $type);
74    }
75
76    public function exec(string $sql): int
77    {
78        try {
79            return $this->wrapped->exec($sql);
80        } catch (\Throwable $e) {
81            if (!$this->isDisconnectException($e)) {
82                throw $e;
83            }
84
85            $this->reconnect();
86
87            return $this->wrapped->exec($sql);
88        }
89    }
90
91    /**
92     * @param string|null $name
93     */
94    public function lastInsertId($name = null): mixed
95    {
96        return $this->wrapped->lastInsertId($name);
97    }
98
99    public function beginTransaction(): mixed
100    {
101        return $this->wrapped->beginTransaction();
102    }
103
104    public function commit(): mixed
105    {
106        return $this->wrapped->commit();
107    }
108
109    public function rollBack(): mixed
110    {
111        return $this->wrapped->rollBack();
112    }
113
114    public function getServerVersion(): mixed
115    {
116        \assert($this->wrapped instanceof ServerInfoAwareConnection);
117
118        return $this->wrapped->getServerVersion();
119    }
120
121    private function reconnect(): void
122    {
123        $this->wrapped = $this->driver->connect($this->params);
124    }
125
126    private function isDisconnectException(\Throwable $e): bool
127    {
128        $message = $e->getMessage();
129
130        return str_contains($message, 'not a valid Interbase link')
131            || str_contains($message, 'isc_lost_db_connection')
132            || str_contains($message, 'connection lost')
133            || str_contains($message, 'Unable to complete network request')
134            || (\is_int($e->getCode()) && $e->getCode() === -902);
135    }
136}