zenstruck / dsn
DSN parsing library with support for complex expressions.
Fund package maintenance!
kbond
Installs: 3 333
Dependents: 1
Suggesters: 0
Security: 0
Stars: 3
Watchers: 3
Forks: 3
Open Issues: 0
Requires
- php: >=8.0
- zenstruck/uri: ^2.0
Requires (Dev)
- phpstan/phpstan: ^1.4
- phpunit/phpunit: ^9.5.0
- symfony/cache: ^5.4|^6.0|^7.0
- symfony/framework-bundle: ^5.4|^6.0|^7.0
- symfony/phpunit-bridge: ^6.0|^7.0
- symfony/var-dumper: ^5.4|^6.0|^7.0
README
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:
Installation
composer require zenstruck/dsn
Usage
Parsing DSNs
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
.
URI
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'
Mailto
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'
Decorated
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'
Group
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'
Complex DSNs
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'
Using Parsed DSNs
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) }
Usage Example
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');
Advanced Usage
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.
Core Parsers
UriParser
Converts url-looking strings to Zenstruck\Uri
objects.
MailtoParser
Converts mailto-looking strings to Zenstruck\Uri\Mailto
objects.
WrappedParser
Converts dsn-function-looking strings to Zenstruck\Dsn\Decorated
or
Zenstruck\Dsn\Group
objects.
Utility Parsers
ChainParser
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.
CacheParser
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)
Custom Parsers
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');
Symfony Bundle
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(...); // ... }
DSN Service Factory
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.