DSN parsing library with support for complex expressions:
- URI:
http://example.com?foo=bar#baz
- Mailto:
mailto:sam@example.com?cc=jane@example.com
- DSN Functions:
composer require zenstruck/dsn
For basic usage, you can use Zenstruck\Dsn::parse($mydsn)
. This takes a string
and returns one of the following objects:
The only thing in common with these returned objects is that they are all \Stringable
.
If the parsing fails, a Zenstruck\Dsn\Exception\UnableToParse
exception will be thrown.
Note See
zenstruck/uri
to view the API forUri|Mailto
.
This DSN object is an instance of Zenstruck\Uri
. View it's
full API documentation.
$dsn = Zenstruck\Dsn::parse('https://example.com/some/dir/file.html?q=abc&flag=1#test')
/* @var Zenstruck\Uri $dsn */
$dsn->scheme()->toString(); // 'https'
$dsn->host()->toString(); // 'example.com'
$dsn->path()->toString(); // /some/dir/file.html
$dsn->query()->all(); // ['q' => 'abc', 'flag' => '1']
$dsn->fragment(); // 'test'
This DSN object is an instance of Zenstruck\Uri\Mailto
. View it's
full API documentation.
$dsn = Zenstruck\Dsn::parse('mailto:kevin@example.com?cc=jane@example.com&subject=some+subject&body=some+body')
/** @var Zenstruck\Uri\Mailto $dsn */
$dsn->to(); // ['kevin@example.com']
$dsn->cc(); // ['jane@example.com']
$dsn->bcc(); // []
$dsn->subject(); // 'some subject'
$dsn->body(); // 'some body'
This is a DSN Function that wraps a single inner DSN:
retry(inner://dsn)?times=5
The above example would parse to a Zenstruck\Dsn\Decorated
object with
the following properties:
- Scheme/Function Name:
retry
- Query:
['times' => '5']
- Inner DSN: This will be an instance of
Zenstruck\Uri
in this case but could be any DSN Object.
$dsn = Zenstruck\Dsn::parse('retry(inner://dsn)?times=5');
/** @var Zenstruck\Dsn\Decorated $dsn */
$dsn->scheme()->toString(); // 'retry'
$dsn->query()->all(); // ['times' => '5']
$inner = $dsn->inner();
/** @var Zenstruck\Uri $inner */
$inner->scheme()->toString(); // 'inner'
$inner->host()->toString(); // 'dsn'
This is a DSN Function that wraps a multiple inner DSNs (space separated):
round+robin(inner://dsn1 inner://dsn2)?strategy=random
The above example would parse to a Zenstruck\Dsn\Group
object with
the following properties:
- Scheme/Function Name:
round+robin
- Query:
['strategy' => 'random']
- Child DSNs: This will be an
array
of 2Zenstruck\Uri
objects in this case but could an array of any DSN Objects.
$dsn = Zenstruck\Dsn::parse('round+robin(inner://dsn1 inner://dsn2)?strategy=random');
/** @var Zenstruck\Dsn\Group $dsn */
$dsn->scheme()->toString(); // 'round+robin'
$dsn->query()->all(); // ['strategy' => 'random']
$children = $dsn->children();
/** @var Zenstruck\Uri[] $children */
$children[0]->scheme()->toString(); // 'inner'
$children[0]->host()->toString(); // 'dsn1'
$children[1]->scheme()->toString(); // 'inner'
$children[1]->host()->toString(); // 'dsn2'
You can nest Group and Decorated DSNs to create complex expressions:
$dsn = Zenstruck\Dsn::parse('retry(round+robin(inner://dsn1 inner://dsn2)?strategy=random)?times=5');
/** @var Zenstruck\Dsn\Decorated $dsn */
$dsn->scheme()->toString(); // 'retry'
$dsn->query()->all(); // ['times' => '5']
$inner = $dsn->inner();
/** @var Zenstruck\Dsn\Group $inner */
$inner->scheme()->toString(); // 'round+robin'
$inner->query()->all(); // ['strategy' => 'random']
$children = $inner->children();
/** @var Zenstruck\Uri[] $children */
$children[0]->scheme()->toString(); // 'inner'
$children[0]->host()->toString(); // 'dsn1'
$children[1]->scheme()->toString(); // 'inner'
$children[1]->host()->toString(); // 'dsn2'
Once parsed, you can use an instanceof
check to determine the type of DSN that
was parsed and act accordingly:
$dsn = Zenstruck\Dsn::parse($someDsnString); // throws Zenstruck\Dsn\Exception\UnableToParse on failure
switch (true) {
case $dsn instanceof Zenstruck\Uri:
// do something with the Uri object
case $dsn instanceof Zenstruck\Uri\Mailto:
// do something with the Mailto object
case $dsn instanceof Decorated:
// do something with the Decorated object (see api below)
case $dsn instanceof Group:
// do something with the Group object (see api below)
}
The best way to show how the parsed DSN could be used for something useful is with an example. Consider an email abstraction library that has multiple service transports (smtp, mailchimp, postmark) and special utility transports: round-robin (for distributing workload between multiple transports) and retry (for retrying failures x times before hard-failing).
You'd like end user's of this library to be able to create transports from a custom DSN syntax. The following is an example of a transport DSN factory:
use Zenstruck\Dsn\Decorated;
use Zenstruck\Dsn\Group;
use Zenstruck\Uri;
class TransportFactory
{
public function create(\Stringable $dsn): TransportInterface
{
if ($dsn instanceof Uri && $dsn->scheme()->equals('smtp')) {
return new SmtpTransport(
host: $dsn->host()->toString(),
user: $dsn->user(),
password: $dsn->pass(),
port: $dsn->port(),
);
}
if ($dsn instanceof Uri && $dsn->scheme()->equals('mailchimp')) {
return new MailchimpTransport(apiKey: $dsn->user());
}
if ($dsn instanceof Uri && $dsn->scheme()->equals('postmark')) {
return new PostmarkTransport(apiKey: $dsn->user());
}
if ($dsn instanceof Decorated && $dsn->scheme()->equals('retry')) {
return new RetryTransport(
transport: $this->create($dsn->inner()), // recursively build inner transport
times: $dsn->query()->getInt('times', 5), // default to 5 retries if not set
);
}
if ($dsn instanceof Group && $dsn->scheme()->equals('round+robin')) {
return new RoundRobinTransport(
transports: array_map(fn($dsn) => $this->create($dsn), $dsn->children()), // recursively build inner transports
strategy: $dsn->query()->get('strategy', 'random'), // default to "random" strategy if not set
);
}
throw new \LogicException("Unable to parse transport DSN: {$dsn}.");
}
}
The usage of this factory is as follows:
use Zenstruck\Dsn;
// SmtpTransport:
$factory->create('smtp://kevin:p4ssword@localhost');
// RetryTransport wrapping SmtpTransport:
$factory->create('retry(smtp://kevin:p4ssword@localhost)');
// RetryTransport (3 retries) wrapping RoundRobinTransport (sequential strategy) wrapping MailchimpTransport & PostmarkTransport
$factory->create('retry(round+robin(mailchimp://key@default postmark://key@default)?strategy=sequential)?times=3');
Under the hood Zenstruck\Dsn::parse()
uses a parsing system for converting DSN
strings to the packaged DSN objects. You can create your own
parsers by having them implement the Zenstruck\Dsn\Parser
interface.
Note
Zenstruck\Dsn::parse()
is a utility function that only uses the core parsers. In order to add your own parsers, you'll need to manually wire up a chain parser that includes them and use this for parsing DSNs.
Converts url-looking strings to Zenstruck\Uri
objects.
Converts mailto-looking strings to Zenstruck\Uri\Mailto
objects.
Converts dsn-function-looking strings to Zenstruck\Dsn\Decorated
or
Zenstruck\Dsn\Group
objects.
Wraps a chain of parsers, during parse()
it loops through these and
attempts to find one that successfully parses a DSN string. It is considered
successful if a \Stringable
object is returned. If the parser throws a
Zenstruck\Dsn\Exception\UnableToParse
exception, the next parser in the
chain is tried. Finally, if all the parsers throw UnableToParse
, this is
thrown.
$parser = new Zenstruck\Dsn\Parser\ChainParser([$customParser1, $customParser1]);
$parser->parse('some-dsn'); // \Stringable object
Note This parser always contains the core parsers as the last items in the chain. Custom parsers you add to the constructor are attempted before these.
Wraps another parser and an instance of one of these cache interfaces:
Symfony\Contracts\Cache\CacheInterface
(Symfony cache)Psr\Cache\CacheItemPoolInterface
(PSR-6 cache)Psr\SimpleCache\CacheInterface
(PSR-16 cache)
The parsed object is cached (keyed by the DSN string) and subsequent parsing of the same string are retrieved from the cache. This gives a bit of a performance boost especially for complex DSNs.
/** @var SymfonyCache|Psr6Cache|Psr16Cache $cache */
/** @var Zenstruck\Dsn\Parser $inner */
$parser = new \Zenstruck\Dsn\Parser\CacheParser($parser, $cache);
$parser->parse('some-dsn'); // \Stringable (caches this object)
$parser->parse('some-dsn'); // \Stringable (retrieved from cache)
You can create your own parser by creating an object that implements
Zenstruck\Dsn\Parser
:
use Zenstruck\Dsn\Exception\UnableToParse;
use Zenstruck\Dsn\Parser;
class MyParser implements Parser
{
public function parse(string $dsn): \Stringable
{
// determine if $dsn is parsable and return a \Stringable DSN object
throw UnableToParse::value($dsn); // important when using in a chain parser
}
}
Usage:
// standalone
$parser = new MyParser();
$parser->parse('some-dsn');
// add to ChainParser
$parser = new Zenstruck\Dsn\Parser\ChainParser([new MyParser()]);
$parser->parse('some-dsn');
A Symfony Bundle is provided that adds an autowireable Zenstruck\Dsn\Parser
service.
This is an interface with a parse(string $dsn)
method. It works identically
to Zenstruck\Dsn::parse()
but caches the created DSN object (using cache.system
)
for a bit of a performance boost.
To use, enable the bundle:
// config/bundles.php
return [
// ...
Zenstruck\Dsn\Bridge\Symfony\ZenstruckDsnBundle::class => ['all' => true],
];
Zenstruck\Dsn\Parser
can be autowired:
use Zenstruck\Dsn\Parser;
public function myAction(Parser $parser): Response
{
// ...
$dsn = $parser->parse(...);
// ...
}
You can use the Zenstruck\Dsn\Parser
service as a service factory to create
DSN service objects:
# config/services.yaml
services:
mailer_dsn:
factory: ['@Zenstruck\Dsn\Parser', 'parse']
arguments: ['%env(MAILER_DSN)%']
The mailer_dsn
service will be an instance of a parsed DSN object. The type
depends on the value of the MAILER_DSN
environment variable.
Using the mailer transport factory above, we can create the
transport via a service factory that uses the mailer_dsn
:
# config/services.yaml
services:
App\Mailer\TransportFactory: ~
App\Mailer\TransportInterface:
factory: ['@App\Mailer\TransportFactory', 'create']
arguments: ['@mailer_dsn']
Now, when injecting App\Mailer\TransportInterface
, the transport will be
created by App\Mailer\TransportFactory
using your MAILER_DSN
environment
variable.