支持复杂表达式的DSN解析库。

资助包维护!
kbond

v0.2.0 2024-01-02 20:47 UTC

This package is auto-updated.

Last update: 2024-08-30 02:05:35 UTC


README

CI codecov

支持复杂表达式的DSN解析库

  1. URI: http://example.com?foo=bar#baz
  2. Mailto: mailto:sam@example.com?cc=jane@example.com
  3. DSN函数:
    1. 装饰: retry(inner://dsn)?times=5
    2. 分组: round+robin(inner://dsn1 inner://dsn2)
    3. 复杂: fail+over(rount+robin(inner://dsn1 inner://dsn2) inner://dsn3)

安装

composer require zenstruck/dsn

使用

解析DSN

对于基本使用,你可以使用 Zenstruck\Dsn::parse($mydsn)。它接受一个 string 并返回以下对象之一

这些返回对象共同的特点是它们都是 \Stringable

如果解析失败,将抛出 Zenstruck\Dsn\Exception\UnableToParse 异常。

注意 请参阅 zenstruck/uri 以查看 Uri|Mailto 的API。

URI

此DSN对象是 Zenstruck\Uri 的实例。查看它的完整API文档

$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

此DSN对象是 Zenstruck\Uri\Mailto 的实例。查看它的完整API文档

$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'

装饰

这是一个包装单个 内部 DSN 的 DSN函数

retry(inner://dsn)?times=5

上面的示例将解析为具有以下属性的 Zenstruck\Dsn\Decorated 对象

  • 方案/函数名: retry
  • 查询: ['times' => '5']
  • 内部DSN: 这将是此情况下的 Zenstruck\Uri 实例,但可能是任何 DSN对象
$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'

分组

这是一个包装多个 内部 DSN(用空格分隔)的 DSN函数

round+robin(inner://dsn1 inner://dsn2)?strategy=random

上面的示例将解析为具有以下属性的 Zenstruck\Dsn\Group 对象

  • 方案/函数名: round+robin
  • 查询: ['strategy' => 'random']
  • 子DSN: 这将是此情况下的2个 Zenstruck\Uri 对象的数组,但可以是任何 DSN对象 的数组。
$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'

复杂DSN

你可以嵌套 分组装饰 DSN以创建复杂表达式。

$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'

使用解析后的DSN

一旦解析,你可以使用 instanceof 检查来确定解析的DSN类型,并相应地操作。

$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)
}

使用示例

展示解析后的DSN如何用于有用的示例的最佳方式是使用示例。考虑一个具有多个 服务传输smtpmailchimppostmark)和特殊 实用传输round-robin(用于在多个传输之间分配工作负载)和 retry(在硬失败之前重试失败x次))的电子邮件抽象库。

您希望该库的最终用户能够使用自定义DSN语法创建传输。以下是一个传输DSN工厂的示例

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}.");
    }
}

该工厂的使用方法如下

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');

高级使用

在底层,Zenstruck\Dsn::parse() 使用一个解析系统将DSN字符串转换为打包的 DSN对象。您可以通过让它们实现 Zenstruck\Dsn\Parser 接口来创建自己的解析器。

注意 Zenstruck\Dsn::parse() 是一个实用函数,它只使用 核心解析器。为了添加您的 自定义解析器,您需要手动设置一个包含它们的 链式解析器 并使用它来解析DSN。

核心解析器

UriParser

将看起来像URL的字符串转换为 Zenstruck\Uri 对象。

MailtoParser

将看起来像mailto的字符串转换为 Zenstruck\Uri\Mailto 对象。

WrappedParser

将看起来像dsn函数的字符串转换为 Zenstruck\Dsn\DecoratedZenstruck\Dsn\Group 对象。

实用解析器

ChainParser

包装一串解析器,在 parse() 期间,它会遍历这些解析器并尝试找到一个成功解析DSN字符串的解析器。如果返回了一个 \Stringable 对象,则被认为是成功的。如果解析器抛出了 Zenstruck\Dsn\Exception\UnableToParse 异常,则尝试链中的下一个解析器。最后,如果所有解析器都抛出了 UnableToParse,则抛出此异常。

$parser = new Zenstruck\Dsn\Parser\ChainParser([$customParser1, $customParser1]);

$parser->parse('some-dsn'); // \Stringable object

注意 此解析器始终将 核心解析器 作为链中的最后几个元素。在构造函数中添加的 自定义解析器 将在它们之前尝试。

CacheParser

包装另一个解析器和一个这些缓存接口之一的实例

  • Symfony\Contracts\Cache\CacheInterface(Symfony缓存)
  • Psr\Cache\CacheItemPoolInterface(PSR-6缓存)
  • Psr\SimpleCache\CacheInterface(PSR-16缓存)

解析的对象将被缓存(键为DSN字符串),并且对同一字符串的后续解析将从缓存中检索。这为 复杂DSN 提供了一点点性能提升。

/** @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)

自定义解析器

您可以通过创建一个实现 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
    }
}

使用

// 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

提供了一个Symfony Bundle,它添加了一个可自动注入的 Zenstruck\Dsn\Parser 服务。这是一个具有 parse(string $dsn) 方法的接口。它与 Zenstruck\Dsn::parse() 的工作方式相同,但通过使用 cache.system 缓存创建的 DSN对象 来提高一点点性能。

要使用,请启用该bundle

// config/bundles.php

return [
    // ...
    Zenstruck\Dsn\Bridge\SymfonyZenstruckDsnBundle::class => ['all' => true],
];

Zenstruck\Dsn\Parser 可以自动注入

use Zenstruck\Dsn\Parser;

public function myAction(Parser $parser): Response
{
    // ...

    $dsn = $parser->parse(...);

    // ...
}

DSN服务工厂

您可以使用 Zenstruck\Dsn\Parser 服务作为服务工厂来创建DSN服务对象

# config/services.yaml

services:
    mailer_dsn:
        factory: ['@Zenstruck\Dsn\Parser', 'parse']
        arguments: ['%env(MAILER_DSN)%']

mailer_dsn 服务将是一个解析后的DSN对象的实例。类型取决于 MAILER_DSN 环境变量的值。

使用上面的 mailer传输工厂,我们可以通过使用 mailer_dsn 的服务工厂来创建传输

# config/services.yaml

services:
    App\Mailer\TransportFactory: ~

    App\Mailer\TransportInterface:
        factory: ['@App\Mailer\TransportFactory', 'create']
        arguments: ['@mailer_dsn']

现在,当注入 App\Mailer\TransportInterface 时,传输将通过 App\Mailer\TransportFactory 使用您的 MAILER_DSN 环境变量来创建。