1# DSN parser
2
3[![Latest Version](https://img.shields.io/github/release/Nyholm/dsn.svg?style=flat-square)](https://github.com/Nyholm/dsn/releases)
4[![Quality Score](https://img.shields.io/scrutinizer/g/Nyholm/dsn.svg?style=flat-square)](https://scrutinizer-ci.com/g/Nyholm/dsn)
5[![SymfonyInsight](https://insight.symfony.com/projects/fe1a70b7-6ba9-424d-9217-53833e47b07f/mini.svg)](https://insight.symfony.com/projects/fe1a70b7-6ba9-424d-9217-53833e47b07f)
6[![Total Downloads](https://img.shields.io/packagist/dt/nyholm/dsn.svg?style=flat-square)](https://packagist.org/packages/nyholm/dsn)
7
8Parse DSN strings into value objects to make them easier to use, pass around and
9manipulate.
10
11## Install
12
13Via Composer
14
15``` bash
16composer require nyholm/dsn
17```
18
19## Quick usage
20
21```php
22use Nyholm\Dsn\DsnParser;
23
24$dsn = DsnParser::parse('http://127.0.0.1/foo/bar?key=value');
25echo get_class($dsn); // "Nyholm\Dsn\Configuration\Url"
26echo $dsn->getHost(); // "127.0.0.1"
27echo $dsn->getPath(); // "/foo/bar"
28echo $dsn->getPort(); // null
29```
30
31## The DSN string format
32A DSN is a string used to configure many services. A common DSN may look like a
33URL, other look like a file path.
34
35```text
36memcached://127.0.0.1
37mysql://user:password@127.0.0.1:3306/my_table
38memcached:///var/local/run/memcached.socket?weight=25
39```
40
41Both types can have parameters, user, password. The exact definition we are using
42is found [at the bottom of the page](#definition).
43
44### DSN Functions
45
46A DSN may contain zero or more functions. The DSN parser supports a function syntax
47but not functionality itself. The function arguments must be separated with space
48or comma. Here are some example functions.
49
50```
51failover(dummy://a dummy://a)
52failover(dummy://a,dummy://a)
53failover:(dummy://a,dummy://a)
54roundrobin(dummy://a failover(dummy://b dummy://a) dummy://b)
55```
56
57## Parsing
58
59There are two methods for parsing; `DsnParser::parse()` and `DsnParser::parseFunc()`.
60The latter is for situations where DSN functions are supported.
61
62```php
63use Nyholm\Dsn\DsnParser;
64
65$dsn = DsnParser::parse('scheme://127.0.0.1/foo/bar?key=value');
66echo get_class($dsn); // "Nyholm\Dsn\Configuration\Url"
67echo $dsn->getHost(); // "127.0.0.1"
68echo $dsn->getPath(); // "/foo/bar"
69echo $dsn->getPort(); // null
70```
71
72If functions are supported (like in the Symfony Mailer component) we can use `DsnParser::parseFunc()`:
73
74```php
75use Nyholm\Dsn\DsnParser;
76
77$func = DsnParser::parseFunc('failover(sendgrid://KEY@default smtp://127.0.0.1)');
78echo $func->getName(); // "failover"
79echo get_class($func->first()); // "Nyholm\Dsn\Configuration\Url"
80echo $func->first()->getHost(); // "default"
81echo $func->first()->getUser(); // "KEY"
82```
83
84```php
85use Nyholm\Dsn\DsnParser;
86
87$func = DsnParser::parseFunc('foo(udp://localhost failover:(tcp://localhost:61616,tcp://remotehost:61616)?initialReconnectDelay=100)?start=now');
88echo $func->getName(); // "foo"
89echo $func->getParameters()['start']; // "now"
90
91$args = $func->getArguments();
92echo get_class($args[0]); // "Nyholm\Dsn\Configuration\Url"
93echo $args[0]->getScheme(); // "udp"
94echo $args[0]->getHost(); // "localhost"
95
96echo get_class($args[1]); // "Nyholm\Dsn\Configuration\DsnFunction"
97```
98
99When using `DsnParser::parseFunc()` on a string that does not contain any DSN functions,
100the parser will automatically add a default "dsn" function. This is added to provide
101a consistent return type of the method.
102
103The string `redis://127.0.0.1` will automatically be converted to `dsn(redis://127.0.0.1)`
104when using `DsnParser::parseFunc()`.
105
106```php
107use Nyholm\Dsn\DsnParser;
108
109$func = DsnParser::parseFunc('smtp://127.0.0.1');
110echo $func->getName(); // "dsn"
111echo get_class($func->first()); // "Nyholm\Dsn\Configuration\Url"
112echo $func->first()->getHost(); // "127.0.0.1"
113
114
115$func = DsnParser::parseFunc('dsn(smtp://127.0.0.1)');
116echo $func->getName(); // "dsn"
117echo get_class($func->first()); // "Nyholm\Dsn\Configuration\Url"
118echo $func->first()->getHost(); // "127.0.0.1"
119```
120
121### Parsing invalid DSN
122
123If you try to parse an invalid DSN string a `InvalidDsnException` will be thrown.
124
125```php
126use Nyholm\Dsn\DsnParser;
127use Nyholm\Dsn\Exception\InvalidDsnException;
128
129try {
130  DsnParser::parse('foobar');
131} catch (InvalidDsnException $e) {
132  echo $e->getMessage();
133}
134```
135
136## Consuming
137
138The result of parsing a DSN string is a `DsnFunction` or `Dsn`. A `DsnFunction` has
139a `name`, `argument` and may have `parameters`. An argument is either a `DsnFunction`
140or a `Dsn`.
141
142A `Dsn` could be a `Path` or `Url`. All 3 objects has methods for getting parts of
143the DSN string.
144
145- `getScheme()`
146- `getUser()`
147- `getPassword()`
148- `getHost()`
149- `getPort()`
150- `getPath()`
151- `getParameters()`
152
153You may also replace parts of the DSN with the `with*` methods. A DSN is immutable
154and you will get a new object back.
155
156```php
157use Nyholm\Dsn\DsnParser;
158
159$dsn = DsnParser::parse('scheme://127.0.0.1/foo/bar?key=value');
160
161echo $dsn->getHost(); // "127.0.0.1"
162$new = $dsn->withHost('nyholm.tech');
163
164echo $dsn->getHost(); // "127.0.0.1"
165echo $new->getHost(); // "nyholm.tech"
166```
167
168## Not supported
169
170### Smart merging of options
171
172The current DSN is valid, but it is up to the consumer to make sure both host1 and
173host2 has `global_option`.
174
175```
176redis://(host1:1234,host2:1234?node2_option=a)?global_option=b
177```
178
179### Special DSN
180
181The following DSN syntax are not supported.
182
183```
184// Rust
185pgsql://user:pass@tcp(localhost:5555)/dbname
186
187// Java
188jdbc:informix-sqli://<server>[:<port>]/<databaseName>:informixserver=<dbservername>
189
190```
191
192We do not support DSN strings for ODBC connections like:
193
194```
195Driver={ODBC Driver 13 for SQL Server};server=localhost;database=WideWorldImporters;trusted_connection=Yes;
196```
197
198However, we do support "only parameters":
199
200```
201ocdb://?Driver=ODBC+Driver+13+for+SQL+Server&server=localhost&database=WideWorldImporters&trusted_connection=Yes
202```
203
204## Definition
205
206There is no official DSN RFC. We have defined a DSN configuration string as
207using the following definition. The "URL looking" parts of a DSN is based from
208[RFC 3986](https://tools.ietf.org/html/rfc3986).
209
210
211```
212configuration:
213  { function | dsn }
214
215function:
216  function_name[:](configuration[,configuration])[?query]
217
218function_name:
219  REGEX: [a-zA-Z0-9\+-]+
220
221dsn:
222  { scheme:[//]authority[path][?query] | scheme:[//][userinfo]path[?query] | host:port[path][?query] }
223
224scheme:
225  REGEX: [a-zA-Z0-9\+-\.]+
226
227authority:
228  [userinfo@]host[:port]
229
230userinfo:
231  { user[:password] | :password }
232
233path:
234  "Normal" URL path according to RFC3986 section 3.3.
235  REGEX: (/? | (/[a-zA-Z0-9-\._~%!\$&'\(\}\*\+,;=:@]+)+)
236
237query:
238  "Normal" URL query according to RFC3986 section 3.4.
239  REGEX: [a-zA-Z0-9-\._~%!\$&'\(\}\*\+,;=:@]+
240
241user:
242  This value can be URL encoded.
243  REGEX: [a-zA-Z0-9-\._~%!\$&'\(\}\*\+,;=]+
244
245password:
246  This value can be URL encoded.
247  REGEX: [a-zA-Z0-9-\._~%!\$&'\(\}\*\+,;=]+
248
249host:
250  REGEX: [a-zA-Z0-9-\._~%!\$&'\(\}\*\+,;=]+
251
252post:
253  REGEX: [0-9]+
254
255```
256
257Example of formats that are supported:
258
259- scheme://127.0.0.1/foo/bar?key=value
260- scheme://user:pass@127.0.0.1/foo/bar?key=value
261- scheme:///var/local/run/memcached.socket?weight=25
262- scheme://user:pass@/var/local/run/memcached.socket?weight=25
263- scheme:?host[localhost]&host[localhost:12345]=3
264- scheme://a
265- scheme://
266- server:80
267