Completed
Push — 2.0 ( 143803...4e64fa )
by grégoire
08:42 queued 04:22
created

Connection::hasHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

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