Completed
Push — master ( 44b855...8a9a36 )
by Ondřej
07:29
created

ConnectionParameters::fromArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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