Completed
Push — master ( 75a4bf...99f4b4 )
by BENOIT
03:23
created

PDOAdapter::createLink()   B

Complexity

Conditions 5
Paths 20

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 14
nc 20
nop 2
1
<?php
2
3
namespace BenTools\SimpleDBAL\Model\Adapter\PDO;
4
5
use BenTools\SimpleDBAL\Contract\AdapterInterface;
6
use BenTools\SimpleDBAL\Contract\CredentialsInterface;
7
use BenTools\SimpleDBAL\Contract\ReconnectableAdapterInterface;
8
use BenTools\SimpleDBAL\Contract\StatementInterface;
9
use BenTools\SimpleDBAL\Contract\ResultInterface;
10
use BenTools\SimpleDBAL\Contract\TransactionAdapterInterface;
11
use BenTools\SimpleDBAL\Model\ConfigurableTrait;
12
use BenTools\SimpleDBAL\Model\Exception\AccessDeniedException;
13
use BenTools\SimpleDBAL\Model\Exception\DBALException;
14
use BenTools\SimpleDBAL\Model\Exception\MaxConnectAttempsException;
15
use BenTools\SimpleDBAL\Model\Exception\ParamBindingException;
16
use GuzzleHttp\Promise\Promise;
17
use GuzzleHttp\Promise\PromiseInterface;
18
use PDO;
19
use PDOException;
20
use Throwable;
21
22
class PDOAdapter implements AdapterInterface, TransactionAdapterInterface, ReconnectableAdapterInterface
23
{
24
    use ConfigurableTrait;
25
26
    /**
27
     * @var PDO
28
     */
29
    private $cnx;
30
31
    /**
32
     * @var CredentialsInterface
33
     */
34
    private $credentials;
35
36
    /**
37
     * @var int
38
     */
39
    private $reconnectAttempts = 0;
40
41
    /**
42
     * PDOAdapter constructor.
43
     * @param PDO $cnx
44
     * @param CredentialsInterface $credentials
45
     * @param array|null $options
46
     */
47
    public function __construct(PDO $cnx, CredentialsInterface $credentials, array $options = null)
48
    {
49
        $this->cnx = $cnx;
50
        if (PDO::ERRMODE_EXCEPTION !== $this->cnx->getAttribute(PDO::ATTR_ERRMODE)) {
51
            $this->cnx->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
52
        }
53
        $this->credentials = $credentials;
54
        if (null !== $options) {
55
            $this->options     = array_replace($this->getDefaultOptions(), $options);
56
        }
57
    }
58
59
    /**
60
     * @inheritDoc
61
     */
62
    public function getWrappedConnection()
63
    {
64
        return $this->cnx;
65
    }
66
67
    /**
68
     * @inheritDoc
69
     */
70
    public function getCredentials(): CredentialsInterface
71
    {
72
        return $this->credentials;
73
    }
74
75
    /**
76
     * @inheritDoc
77
     */
78
    public function isConnected(): bool
79
    {
80
        try {
81
            self::wrapWithErrorHandler(function () {
82
                $this->cnx->getAttribute(PDO::ATTR_SERVER_INFO);
83
            });
84
            return true;
85
        } catch (Throwable $e) {
86
            return false;
87
        }
88
    }
89
90
    /**
91
     * @inheritDoc
92
     */
93
    public function shouldReconnect(): bool
94
    {
95
        return !$this->isConnected() && $this->reconnectAttempts < (int) $this->getOption(self::OPT_MAX_RECONNECT_ATTEMPTS);
96
    }
97
98
    /**
99
     * Tries to reconnect to database.
100
     */
101
    private function reconnect()
102
    {
103
        if (0 === (int) $this->getOption(self::OPT_MAX_RECONNECT_ATTEMPTS)) {
104
            throw new MaxConnectAttempsException("Connection lost.");
105
        } elseif ($this->reconnectAttempts === (int) $this->getOption(self::OPT_MAX_RECONNECT_ATTEMPTS)) {
106
            throw new MaxConnectAttempsException("Max attempts to connect to database has been reached.");
107
        }
108
        try {
109
            if (0 !== $this->reconnectAttempts) {
110
                usleep((int) $this->getOption(self::OPT_USLEEP_AFTER_FIRST_ATTEMPT));
111
            }
112
            $this->cnx = self::createLink($this->getCredentials(), $this->options);
113
            if ($this->isConnected()) {
114
                $this->reconnectAttempts = 0;
115
            } else {
116
                $this->reconnect();
117
            }
118
        } catch (Throwable $e) {
119
            $this->reconnectAttempts++;
120
        }
121
    }
122
123
    /**
124
     * @inheritDoc
125
     */
126
    public function prepare(string $query, array $values = null): StatementInterface
127
    {
128
        try {
129
            $wrappedStmt = $this->cnx->prepare($query);
130
        } catch (PDOException $e) {
131
            if (!$this->isConnected()) {
132
                $this->reconnect();
133
                return $this->prepare($query, $values);
134
            }
135
            throw new DBALException($e->getMessage(), (int) $e->getCode(), $e);
136
        }
137
        return new Statement($this, $wrappedStmt, $values);
138
    }
139
140
    /**
141
     * @inheritDoc
142
     */
143
    public function execute($stmt, array $values = null): ResultInterface
144
    {
145
        if (is_string($stmt)) {
146
            $stmt = $this->prepare($stmt);
147
        }
148
        if (!$stmt instanceof Statement) {
149
            throw new \InvalidArgumentException(sprintf('Expected %s object, got %s', Statement::class, get_class($stmt)));
150
        }
151
        if (null !== $values) {
152
            $stmt = $stmt->withValues($values);
153
        }
154
        try {
155
            $this->runStmt($stmt);
156
            $result = $stmt->createResult();
157
        } catch (Throwable $e) {
158
            if (!$this->isConnected()) {
159
                $this->reconnect();
160
                return $this->execute($this->prepare((string) $stmt, $stmt->getValues()));
161
            }
162
            throw $e;
163
        }
164
        return $result;
165
    }
166
167
    /**
168
     * @inheritDoc
169
     */
170
    public function executeAsync($stmt, array $values = null): PromiseInterface
171
    {
172
        $promise = new Promise(function () use (&$promise, $stmt, $values) {
173
            try {
174
                $promise->resolve($this->execute($stmt, $values));
175
            } catch (DBALException $e) {
176
                $promise->reject($e);
177
            }
178
        });
179
        return $promise;
180
    }
181
182
    /**
183
     * @param \PDOStatement $wrappedStmt
0 ignored issues
show
Bug introduced by
There is no parameter named $wrappedStmt. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
184
     */
185
    private function runStmt(Statement $stmt)
186
    {
187
        $wrappedStmt = $stmt->getWrappedStatement();
188
        try {
189
            self::wrapWithErrorHandler(function () use ($stmt, $wrappedStmt) {
190
                $stmt->bind();
191
                $wrappedStmt->execute();
192
            });
193
        } catch (\PDOException $e) {
194
            if (false !== strpos($e->getMessage(), 'no parameters were bound')) {
195
                throw new ParamBindingException($e->getMessage(), (int) $e->getCode(), $e, $stmt);
196
            }
197
            if (false !== strpos($e->getMessage(), 'number of bound variables does not match number')) {
198
                throw new ParamBindingException($e->getMessage(), (int) $e->getCode(), $e, $stmt);
199
            }
200
            throw new DBALException($e->getMessage(), (int) $e->getCode(), $e);
201
        }
202
    }
203
204
    /**
205
     * @inheritDoc
206
     */
207
    public function beginTransaction()
208
    {
209
        $this->getWrappedConnection()->beginTransaction();
210
    }
211
212
    /**
213
     * @inheritDoc
214
     */
215
    public function commit()
216
    {
217
        $this->getWrappedConnection()->commit();
218
    }
219
220
    /**
221
     * @inheritDoc
222
     */
223
    public function rollback()
224
    {
225
        $this->getWrappedConnection()->rollBack();
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231
    public function getDefaultOptions(): array
232
    {
233
        return [
234
            self::OPT_MAX_RECONNECT_ATTEMPTS => self::DEFAULT_MAX_RECONNECT_ATTEMPTS,
235
            self::OPT_USLEEP_AFTER_FIRST_ATTEMPT => self::DEFAULT_USLEEP_AFTER_FIRST_ATTEMPT,
236
        ];
237
    }
238
239
    /**
240
     * @param CredentialsInterface $credentials
241
     * @return PDOAdapter
242
     */
243
    public static function factory(CredentialsInterface $credentials, array $options = null): self
244
    {
245
        return new static(self::createLink($credentials, $options), $credentials, $options);
246
    }
247
248
    /**
249
     * @param CredentialsInterface $credentials
250
     * @return PDO
251
     */
252
    private static function createLink(CredentialsInterface $credentials, array $options = null): PDO
253
    {
254
        $dsn = sprintf('%s:', $credentials->getPlatform());
255
        $dsn .= sprintf('host=%s;', $credentials->getHostname());
256
        if (null !== $credentials->getPort()) {
257
            $dsn .= sprintf('port=%s;', $credentials->getPort());
258
        }
259
        if (null !== $credentials->getDatabase()) {
260
            $dsn .= sprintf('dbname=%s;', $credentials->getDatabase());
261
        }
262
        try {
263
            $pdoOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
264
            if (isset($options['charset'])) {
265
                $pdoOptions[PDO::MYSQL_ATTR_INIT_COMMAND] = sprintf('SET NAMES %s', $options['charset']);
266
            }
267
            return new PDO($dsn, $credentials->getUser(), $credentials->getPassword(), $pdoOptions);
268
        } catch (\PDOException $e) {
269
            throw new AccessDeniedException($e->getMessage(), (int) $e->getCode(), $e);
270
        }
271
    }
272
273
274
    /**
275
     * @param callable $run
276
     * @return mixed|void
277
     */
278
    private static function wrapWithErrorHandler(callable $run)
279
    {
280
        $errorHandler = function ($errno, $errstr) {
281
            throw new PDOException($errstr, $errno);
282
        };
283
        set_error_handler($errorHandler, E_WARNING);
284
        $result = $run();
285
        restore_error_handler();
286
        return $result;
287
    }
288
}
289