Completed
Push — master ( 8a9a36...5d30b0 )
by Ondřej
03:00
created

ConnectionParameters::buildConnectionString()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 11
nc 4
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Ivory\Connection;
5
6
use Ivory\Exception\UnsupportedException;
7
8
/**
9
 * Parameters of a database connection.
10
 *
11
 * @todo consider introducing constants for standard parameters (http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS)
12
 * @todo document usage of \ArrayAccess and \IteratorAggregate interfaces
13
 */
14
class ConnectionParameters implements \ArrayAccess, \IteratorAggregate
15
{
16
    private $params = [];
17
18
    /**
19
     * Create connection parameters from an array, a URI, or a connection string.
20
     *
21
     * For details on passing:
22
     * - an array, see {@link fromArray()};
23
     * - a URI, see {@link fromUri()};
24
     * - a connection string, see {@link fromConnectionString()}.
25
     *
26
     * If a `ConnectionParameters` object is given, a clone is returned.
27
     *
28
     * @param array|string|ConnectionParameters $params array of parameters, or URI, or connection string, or object
29
     * @return ConnectionParameters
30
     */
31
    public static function create($params): ConnectionParameters
32
    {
33
        if (is_array($params)) {
34
            return self::fromArray($params);
35
        } elseif (is_string($params)) {
36
            if (preg_match("~^[^=']+://~", $params)) {
37
                return self::fromUri($params);
38
            } else {
39
                return self::fromConnectionString($params);
40
            }
41
        } elseif ($params instanceof ConnectionParameters) {
42
            return clone $params;
43
        } else {
44
            throw new \InvalidArgumentException('params');
45
        }
46
    }
47
48
    /**
49
     * Initializes the connection parameters from an associative array of keywords to values.
50
     *
51
     * The most important are the following parameters:
52
     * - `host (string)`: the database server to connect to,
53
     * - `port (int)`: the port to connect to,
54
     * - `user (string)`: username to authenticate as,
55
     * - `password (string)`: password for the given username,
56
     * - `dbname (string)`: name of the database to connect to,
57
     * - `connect_timeout (int)`: connection timeout (0 means to wait indefinitely),
58
     * - `options (string)`: the runtime options to send to the server.
59
     *
60
     * For details, see the
61
     * {@link http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS PostgreSQL documentation}.
62
     * Any parameter may be omitted - the default is used then.
63
     *
64
     * @param array $params map: connection parameter keyword => value
65
     * @return ConnectionParameters
66
     */
67
    public static function fromArray(array $params): ConnectionParameters
68
    {
69
        return new ConnectionParameters($params);
70
    }
71
72
    /**
73
     * Creates a connection parameters object from an RFC 3986 URI, e.g., `"postgresql://usr@localhost:5433/db"`.
74
     *
75
     * The accepted URI is the same as for the libpq connect function, described in the
76
     * {@link http://www.postgresql.org/docs/9.4/static/libpq-connect.html PostgreSQL documentation}.
77
     *
78
     * The following holds for the accepted URIs:
79
     * - the URI scheme designator must either be `"postgresql"` or `"postgres"`,
80
     * - any part of the URI is optional,
81
     * - username and password are used as credentials when connecting,
82
     * - server and port specify the server and port to connect to,
83
     * - the path specifies the name of the database to connect to,
84
     * - URI parameters are also supported, e.g., `"postgresql:///mydb?host=localhost&port=5433"`.
85
     *
86
     * @param string $uri
87
     * @return ConnectionParameters
88
     */
89
    public static function fromUri(string $uri): ConnectionParameters
90
    {
91
        $c = parse_url($uri);
92
        if ($c === false) {
93
            // NOTE: parse_url() denies the input if the host part is omitted, even though RFC 3986 says it is optional
94
            $auxUri = preg_replace('~//~', '//host', $uri, 1, $found); // NOTE: only preg_replace has a limit :-(
95
            if ($found == 0 || ($c = parse_url($auxUri)) === false) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $found of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
96
                throw new \InvalidArgumentException('uri is malformed');
97
            }
98
            unset($c['host']);
99
        }
100
        if (!isset($c['scheme'])) {
101
            throw new \InvalidArgumentException('uri scheme not specified');
102
        }
103
        if ($c['scheme'] != 'postgresql' && $c['scheme'] != 'postgres') {
104
            throw new UnsupportedException('Only "postgresql" or "postgres" scheme is supported');
105
        }
106
107
        $params = array_filter(
108
            [
109
                'host' => ($c['host'] ?? null),
110
                'port' => ($c['port'] ?? null),
111
                'dbname' => (isset($c['path']) && strlen($c['path']) > 1 ? substr($c['path'], 1) : null),
112
                'user' => ($c['user'] ?? null),
113
                'password' => ($c['pass'] ?? null),
114
            ],
115
            'strlen'
116
        );
117
        if (isset($c['query'])) {
118
            parse_str($c['query'], $pars);
119
            $params = array_merge($params, $pars);
120
        }
121
122
        foreach ($params as &$par) {
123
            $par = rawurldecode((string)$par); // NOTE: neither parse_url() nor parse_str() do that automatically
124
        }
125
126
        return new ConnectionParameters($params);
127
    }
128
129
    /**
130
     * Creates a connection parameters object from a PostgreSQL connection string (see {@link pg_connect()}).
131
     *
132
     * A connection string is a set of `keyword = value` pairs, separated by space. Spaces around the equal sign are
133
     * optional. To contain special characters, the value may be enclosed in single quotes, using backslash as the
134
     * escape character.
135
     *
136
     * For details about the connection parameter keywords and values, see {@link __construct()}.
137
     *
138
     * @param string $connStr a PostgreSQL connection string
139
     * @return ConnectionParameters
140
     */
141
    public static function fromConnectionString(string $connStr): ConnectionParameters
142
    {
143
        $params = [];
144
        $keyValueRegEx = "~\\s*([^=\\s]+)\\s*=\\s*([^'\\s]+|'(?:[^'\\\\]|\\\\['\\\\])*')~";
145
        $offset = 0;
146
        while (preg_match($keyValueRegEx, $connStr, $m, 0, $offset)) {
147
            $k = $m[1];
148
            $v = $m[2];
149
            if ($v[0] == "'") {
150
                $v = strtr(substr($v, 1, -1), ["\\'" => "'", '\\\\' => '\\']);
151
            }
152
            $params[$k] = $v;
153
            $offset += strlen($m[0]);
154
        }
155
        if (strlen(trim(substr($connStr, $offset))) > 0) {
156
            throw new \InvalidArgumentException('connStr');
157
        }
158
159
        return new ConnectionParameters($params);
160
    }
161
162
    protected function __construct(array $params)
163
    {
164
        $this->params = $params;
165
    }
166
167
    /**
168
     * @return string connection string suitable for the pg_connect() function
169
     */
170
    public function buildConnectionString(): string
171
    {
172
        $kvPairs = [];
173
        foreach ($this->params as $k => $v) {
174
            if ($v === null) {
175
                continue;
176
            }
177
178
            if (strlen($v) == 0 || preg_match("~[\\s']~", $v)) {
179
                $vstr = "'" . strtr($v, ["'" => "\\'", '\\' => '\\\\']) . "'";
180
            } else {
181
                $vstr = $v;
182
            }
183
184
            $kvPairs[] = $k . '=' . $vstr;
185
        }
186
187
        return implode(' ', $kvPairs);
188
    }
189
190
191
    public function getHost(): ?string
192
    {
193
        return ($this->params['host'] ?? null);
194
    }
195
196
    public function getPort(): ?int
197
    {
198
        return (isset($this->params['port']) ? (int)$this->params['port'] : null);
199
    }
200
201
    public function getDbName(): ?string
202
    {
203
        return ($this->params['dbname'] ?? null);
204
    }
205
206
    public function getUsername(): ?string
207
    {
208
        return ($this->params['user'] ?? null);
209
    }
210
211
    public function getPassword(): ?string
212
    {
213
        return ($this->params['password'] ?? null);
214
    }
215
216
217
    public function getIterator()
218
    {
219
        return new \ArrayIterator($this->params);
220
    }
221
222
223
    public function offsetExists($offset)
224
    {
225
        return array_key_exists($offset, $this->params);
226
    }
227
228
    public function offsetGet($offset)
229
    {
230
        return $this->params[$offset];
231
    }
232
233
    public function offsetSet($offset, $value)
234
    {
235
        $this->params[$offset] = ($value === null ? null : (string)$value);
236
    }
237
238
    public function offsetUnset($offset)
239
    {
240
        unset($this->params[$offset]);
241
    }
242
}
243