Connection::sendPrepareQuery()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
/*
3
 * This file is part of the Pomm's Foundation package.
4
 *
5
 * (c) 2014 - 2017 Grégoire HUBERT <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace PommProject\Foundation\Session;
11
12
use PommProject\Foundation\Exception\ConnectionException;
13
use PommProject\Foundation\Exception\SqlException;
14
15
/**
16
 * Connection
17
 *
18
 * Manage connection through a resource handler.
19
 *
20
 * @package   Foundation
21
 * @copyright 2014 - 2017 Grégoire HUBERT
22
 * @author    Grégoire HUBERT
23
 * @license   X11 {@link http://opensource.org/licenses/mit-license.php}
24
 */
25
class Connection
26
{
27
    const CONNECTION_STATUS_NONE    = 0;
28
    const CONNECTION_STATUS_GOOD    = 1;
29
    const CONNECTION_STATUS_BAD     = 2;
30
    const CONNECTION_STATUS_CLOSED  = 3;
31
    const ISOLATION_READ_COMMITTED  = "READ COMMITTED";  // default
32
    const ISOLATION_REPEATABLE_READ = "REPEATABLE READ"; // from Pg 9.1
33
    const ISOLATION_SERIALIZABLE    = "SERIALIZABLE";    // changes in 9.1
34
    const CONSTRAINTS_DEFERRED      = "DEFERRED";
35
    const CONSTRAINTS_IMMEDIATE     = "IMMEDIATE";       // default
36
    const ACCESS_MODE_READ_ONLY     = "READ ONLY";
37
    const ACCESS_MODE_READ_WRITE    = "READ WRITE";      // default
38
39
    protected $handler;
40
    protected $configurator;
41
    private $is_closed = false;
42
43
    /**
44
     * __construct
45
     *
46
     * Constructor. Test if the given DSN is valid.
47
     *
48
     * @param  string $dsn
49
     * @param  bool $persist
50
     * @param  array $configuration
51
     * @throws ConnectionException if pgsql extension is missing
52
     */
53
    public function __construct($dsn, $persist = false, array $configuration = [])
54
    {
55
        if (!function_exists('pg_connection_status')) {
56
            $message = <<<ERROR
57
`pgsql` PHP extension's functions are unavailable in your environment.
58
Please make sure the _pgsql_ module (not PDO) is enabled in your PHP
59
configuration.
60
ERROR;
61
            throw new ConnectionException($message);
62
        }
63
64
        $this->configurator = new ConnectionConfigurator($dsn, $persist);
65
        $this->configurator->addConfiguration($configuration);
66
    }
67
68
    /**
69
     * close
70
     *
71
     * Close the connection if any.
72
     *
73
     * @return Connection $this
74
     */
75
    public function close()
76
    {
77
        if ($this->hasHandler()) {
78
            pg_close($this->handler);
79
            $this->handler = null;
80
            $this->is_closed = true;
81
        }
82
83
        return $this;
84
    }
85
86
    /**
87
     * addConfiguration
88
     *
89
     * Add configuration settings. If settings exist, they are overridden.
90
     *
91
     * @param  array               $configuration
92
     * @throws  ConnectionException if connection is already up.
93
     * @return Connection          $this
94
     */
95
    public function addConfiguration(array $configuration)
96
    {
97
        $this
98
            ->checkConnectionUp("Cannot update configuration once the connection is open.")
99
            ->configurator->addConfiguration($configuration);
100
101
        return $this;
102
    }
103
104
    /**
105
     * addConfigurationSetting
106
     *
107
     * Add or override a configuration definition.
108
     *
109
     * @param  string     $name
110
     * @param  string     $value
111
     * @return Connection
112
     */
113
    public function addConfigurationSetting($name, $value)
114
    {
115
        $this->checkConnectionUp("Cannot set configuration once a connection is made with the server.")
116
            ->configurator->set($name, $value);
117
118
        return $this;
119
    }
120
121
    /**
122
     * getHandler
123
     *
124
     * Return the connection handler. If no connection are open, it opens one.
125
     *
126
     * @throws  ConnectionException if connection is open in a bad state.
127
     * @return resource
128
     */
129
    protected function getHandler()
130
    {
131
        switch ($this->getConnectionStatus()) {
132
            case static::CONNECTION_STATUS_NONE:
133
                $this->launch();
134
                // no break
135
            case static::CONNECTION_STATUS_GOOD:
136
                return $this->handler;
137
            case static::CONNECTION_STATUS_BAD:
138
                throw new ConnectionException(
139
                    "Connection problem. Read your server's log about this, I have no more informations."
140
                );
141
            case static::CONNECTION_STATUS_CLOSED:
142
                throw new ConnectionException(
143
                    "Connection has been closed, no further queries can be sent."
144
                );
145
        }
146
    }
147
148
    /**
149
     * hasHandler
150
     *
151
     * Tell if a handler is set or not.
152
     *
153
     * @return bool
154
     */
155
    protected function hasHandler()
156
    {
157
        return (bool) ($this->handler !== null);
158
    }
159
160
    /**
161
     * getConnectionStatus
162
     *
163
     * Return a connection status.
164
     *
165
     * @return int
166
     */
167
    public function getConnectionStatus()
168
    {
169
        if (!$this->hasHandler()) {
170
            if ($this->is_closed) {
171
                return static::CONNECTION_STATUS_CLOSED;
172
            } else {
173
                return static::CONNECTION_STATUS_NONE;
174
            }
175
        }
176
177
        if (@pg_connection_status($this->handler) === \PGSQL_CONNECTION_OK) {
178
            return static::CONNECTION_STATUS_GOOD;
179
        }
180
181
        return static::CONNECTION_STATUS_BAD;
182
    }
183
184
    /**
185
     * getTransactionStatus
186
     *
187
     * Return the current transaction status.
188
     * Return a PHP constant.
189
     * @see http://fr2.php.net/manual/en/function.pg-transaction-status.php
190
     *
191
     * @return int
192
     */
193
    public function getTransactionStatus()
194
    {
195
        return pg_transaction_status($this->handler);
196
    }
197
198
    /**
199
     * launch
200
     *
201
     * Open a connection on the database.
202
     *
203
     * @throws  ConnectionException if connection fails.
204
     * return  Connection $this
205
     */
206
    private function launch()
207
    {
208
        $string = $this->configurator->getConnectionString();
209
        $persist = $this->configurator->getPersist();
210
211
        if ($persist) {
212
            $handler = pg_pconnect($string);
213
        } else {
214
            $handler = pg_connect($string, \PGSQL_CONNECT_FORCE_NEW);
215
        }
216
217
        if ($handler === false) {
218
            throw new ConnectionException(
219
                sprintf(
220
                    "Error connecting to the database with parameters '%s'.",
221
                    preg_replace('/password=[^ ]+/', 'password=xxxx', $string)
222
                )
223
            );
224
        } else {
225
            $this->handler = $handler;
226
        }
227
228
        if ($this->getConnectionStatus() !== static::CONNECTION_STATUS_GOOD) {
229
            throw new ConnectionException(
230
                "Connection open but in a bad state. Read your database server log to learn more about this."
231
            );
232
        }
233
234
        $this->sendConfiguration();
235
236
        return $this;
237
    }
238
239
    /**
240
     * sendConfiguration
241
     *
242
     * Send the configuration settings to the server.
243
     *
244
     * @return Connection $this
245
     */
246
    protected function sendConfiguration()
247
    {
248
        $sql=[];
249
250
        foreach ($this->configurator->getConfiguration() as $setting => $value) {
251
            $sql[] = sprintf("set %s = %s", pg_escape_identifier($this->handler, $setting), pg_escape_literal($this->handler, $value));
252
        }
253
254
        if (count($sql) > 0) {
255
            $this->testQuery(
256
                pg_query($this->getHandler(), join('; ', $sql)),
257
                sprintf("Error while applying settings '%s'.", join('; ', $sql))
258
            );
259
        }
260
261
        return $this;
262
    }
263
264
    /**
265
     * checkConnectionUp
266
     *
267
     * Check if the handler is set and throw an Exception if yes.
268
     *
269
     * @param  string     $error_message
270
     * @throws ConnectionException
271
     * @return Connection $this
272
     */
273
    private function checkConnectionUp($error_message = '')
274
    {
275
        if ($this->hasHandler()) {
276
            if ($error_message === '') {
277
                $error_message = "Connection is already made with the server";
278
            }
279
280
            throw new ConnectionException($error_message);
281
        }
282
283
        return $this;
284
    }
285
286
    /**
287
     * executeAnonymousQuery
288
     *
289
     * Performs a raw SQL query
290
     *
291
     * @param  string              $sql The sql statement to execute.
292
     * @return ResultHandler|array
293
     */
294
    public function executeAnonymousQuery($sql)
295
    {
296
        $ret = pg_send_query($this->getHandler(), $sql);
297
298
        return $this
299
            ->testQuery($ret, sprintf("Anonymous query failed '%s'.", $sql))
300
            ->getQueryResult($sql)
301
            ;
302
    }
303
304
    /**
305
     * getQueryResult
306
     *
307
     * Get an asynchronous query result.
308
     * The only reason for the SQL query to be passed as parameter is to throw
309
     * a meaningful exception when an error is raised.
310
     * Since it is possible to send several queries at a time, This method can
311
     * return an array of ResultHandler.
312
     *
313
     * @param  string $sql  (default null)
314
     * @throws ConnectionException if no response are available.
315
     * @throws SqlException if the result is an error.
316
     * @return ResultHandler|array
317
     */
318
    protected function getQueryResult($sql = null)
319
    {
320
        $results = [];
321
322
        while ($result = pg_get_result($this->getHandler())) {
323
            $status = pg_result_status($result, \PGSQL_STATUS_LONG);
324
325
            if ($status !== \PGSQL_COMMAND_OK && $status !== \PGSQL_TUPLES_OK) {
326
                throw new SqlException($result, $sql);
327
            }
328
329
            $results[] = new ResultHandler($result);
330
        }
331
332
        if (count($results) === 0) {
333
            throw new ConnectionException(
334
                sprintf(
335
                    "There are no waiting results in connection.\nQuery = '%s'.",
336
                    $sql
337
                )
338
            );
339
        }
340
341
        return count($results) === 1 ? $results[0] : $results;
342
    }
343
344
    /**
345
     * escapeIdentifier
346
     *
347
     * Escape database object's names. This is different from value escaping
348
     * as objects names are surrounded by double quotes. API function does
349
     * provide a nice escaping with -- hopefully -- UTF8 support.
350
     *
351
     * @see http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html
352
     * @param  string $string The string to be escaped.
353
     * @return string the escaped string.
354
     */
355
    public function escapeIdentifier($string)
356
    {
357
        return \pg_escape_identifier($this->getHandler(), $string);
358
    }
359
360
    /**
361
     * escapeLiteral
362
     *
363
     * Escape a text value.
364
     *
365
     * @param  string $string The string to be escaped
366
     * @return string the escaped string.
367
     */
368
    public function escapeLiteral($string)
369
    {
370
        return \pg_escape_literal($this->getHandler(), $string);
371
    }
372
373
    /**
374
     * escapeBytea
375
     *
376
     * Wrap pg_escape_bytea
377
     *
378
     * @param  string $word
379
     * @return string
380
     */
381
    public function escapeBytea($word)
382
    {
383
        return pg_escape_bytea($this->getHandler(), $word);
384
    }
385
386
    /**
387
     * unescapeBytea
388
     *
389
     * Unescape PostgreSQL bytea.
390
     *
391
     * @param  string $bytea
392
     * @return string
393
     */
394
    public function unescapeBytea($bytea)
395
    {
396
        return pg_unescape_bytea($bytea);
397
    }
398
399
    /**
400
     * sendQueryWithParameters
401
     *
402
     * Execute a asynchronous query with parameters and send the results.
403
     *
404
     * @param  string        $query
405
     * @param  array         $parameters
406
     * @throws SqlException
407
     * @return ResultHandler|array query result wrapper
408
     */
409
    public function sendQueryWithParameters($query, array $parameters = [])
410
    {
411
        $res = pg_send_query_params(
412
            $this->getHandler(),
413
            $query,
414
            $parameters
415
        );
416
417
        try {
418
            return $this
419
                ->testQuery($res, $query)
420
                ->getQueryResult($query)
421
                ;
422
        } catch (SqlException $e) {
423
            throw $e->setQueryParameters($parameters);
424
        }
425
    }
426
427
    /**
428
     * sendPrepareQuery
429
     *
430
     * Send a prepare query statement to the server.
431
     *
432
     * @param  string     $identifier
433
     * @param  string     $sql
434
     * @return Connection $this
435
     */
436
    public function sendPrepareQuery($identifier, $sql)
437
    {
438
        $this
439
            ->testQuery(
440
                pg_send_prepare($this->getHandler(), $identifier, $sql),
441
                sprintf("Could not send prepare statement «%s».", $sql)
442
            )
443
            ->getQueryResult(sprintf("PREPARE ===\n%s\n ===", $sql))
444
            ;
445
446
        return $this;
447
    }
448
449
    /**
450
     * testQueryAndGetResult
451
     *
452
     * Factor method to test query return and summon getQueryResult().
453
     *
454
     * @param  mixed      $query_return
455
     * @param  string     $sql
456
     * @throws ConnectionException
457
     * @return Connection $this
458
     */
459
    protected function testQuery($query_return, $sql)
460
    {
461
        if ($query_return === false) {
462
            throw new ConnectionException(sprintf("Query Error : '%s'.", $sql));
463
        }
464
465
        return $this;
466
    }
467
468
    /**
469
     * sendExecuteQuery
470
     *
471
     * Execute a prepared statement.
472
     * The optional SQL parameter is for debugging purposes only.
473
     *
474
     * @param  string        $identifier
475
     * @param  array         $parameters
476
     * @param  string        $sql
477
     * @return ResultHandler|array
478
     */
479
    public function sendExecuteQuery($identifier, array $parameters = [], $sql = '')
480
    {
481
        $ret = pg_send_execute($this->getHandler(), $identifier, $parameters);
482
483
        return $this
484
            ->testQuery($ret, sprintf("Prepared query '%s'.", $identifier))
485
            ->getQueryResult(sprintf("EXECUTE ===\n%s\n ===\nparameters = {%s}", $sql, join(', ', $parameters)))
486
            ;
487
    }
488
489
    /**
490
     * getClientEncoding
491
     *
492
     * Return the actual client encoding.
493
     *
494
     * @return string
495
     */
496
    public function getClientEncoding()
497
    {
498
        $encoding = pg_client_encoding($this->getHandler());
499
        $this->testQuery($encoding, 'get client encoding');
500
501
        return $encoding;
502
    }
503
504
    /**
505
     * setClientEncoding
506
     *
507
     * Set client encoding.
508
     *
509
     * @param  string     $encoding
510
     * @return Connection $this;
511
     */
512
    public function setClientEncoding($encoding)
513
    {
514
        $result = pg_set_client_encoding($this->getHandler(), $encoding);
515
516
        return $this
517
            ->testQuery((bool) ($result != -1), sprintf("Set client encoding to '%s'.", $encoding))
518
            ;
519
    }
520
521
    /**
522
     * getNotification
523
     *
524
     * Get pending notifications. If no notifications are waiting, NULL is
525
     * returned. Otherwise an associative array containing the optional data
526
     * and de backend's PID is returned.
527
     *
528
     * @return array|null
529
     */
530
    public function getNotification()
531
    {
532
        $data = pg_get_notify($this->handler, \PGSQL_ASSOC);
533
534
        return $data === false ? null : $data;
535
    }
536
}
537