对象包装器/操作器,用于解析URL,并具有额外功能。

资助包维护!
kbond

v2.3.1 2023-12-20 23:48 UTC

This package is auto-updated.

Last update: 2024-09-16 01:58:35 UTC


README

CI codecov

parse_url的对象包装器/操作器,具有以下功能

  • 将URI部分(SchemaHostPathQuery)作为对象读取,每个对象都有其自己的功能集。
  • 操作URI部分或使用流畅的构建器API构建URI。
  • 签名和验证 URI,并使其临时和/或单次使用。
  • Mailto对象,帮助读取/操作mailto: URI。
  • URI模板RFC 6570)支持。
  • PSR-13链接实现/桥接。
  • Twig扩展.

此库旨在作为PHP的parse_url函数的包装器,并不符合任何URI相关的PSR或RFC。如果您需要这个功能,league/uri将是更好的选择。

安装

composer require zenstruck/uri

解析/读取URI

use Zenstruck\Uri\ParsedUri;

// wrap a uri (this URI will be used for many of the samples below)
$uri = ParsedUri::wrap('https://username:[email protected]/some/dir/file.html?q=abc&flag=1#test');

// can wrap an instance of \Symfony\Component\HttpFoundation\Request
$uri = ParsedUri::wrap($request);

// URIs are stringable
$uri->toString();
(string) $uri;

// check if absolute
$uri->isAbsolute(); // true
ParsedUri::wrap('/some/path/only')->isAbsolute(); // false

// SCHEME
$uri->scheme()->toString(); // "https"
$uri->scheme()->equals('https'); // true
$uri->scheme()->in(['https', 'http']); // true

// scheme segments - ie some kind of dsn (delimiter defaults to "+")
ParsedUri::wrap('postmark+smtp://id')->scheme()->segments(); // ["postmark", "smtp"]
ParsedUri::wrap('postmark+smtp://id')->scheme()->segment(0); // "postmark"
ParsedUri::wrap('postmark+smtp://id')->scheme()->segment(1); // "smtp"
ParsedUri::wrap('postmark+smtp://id')->scheme()->segment(2); // null
ParsedUri::wrap('postmark+smtp://id')->scheme()->segment(2, 'default'); // "default"
ParsedUri::wrap('postmark+smtp://id')->scheme()->contains('postmark'); // true

// customize the delimiter
ParsedUri::wrap('postmark-smtp://id')->scheme()->segments('-'); // ["postmark", "smtp"]
ParsedUri::wrap('postmark-smtp://id')->scheme()->segment(0, delimiter: '-'); // ["postmark", "smtp"]
ParsedUri::wrap('postmark-smtp://id')->scheme()->contains('postmark', delimiter: '-'); // true

// HOST
$uri->host()->toString(); // example.com
$uri->host()->segments(); // ["example", "com"]
$uri->host()->segment(0); // "example"
$uri->host()->tld(); // "com"

// USER/PASS
$uri->username(); // "username"
$uri->password(); // "password"

ParsedUri::wrap('http://foo%40bar.com:pass%[email protected]')->username(); // [email protected] (urldecoded)
ParsedUri::wrap('http://foo%40bar.com:pass%[email protected]')->password(); // pass#word (urldecoded)

// PORT
$uri->port(); // (null)
ParsedUri::wrap('example.com:21')->port(); // 21

// guess port from scheme
ParsedUri::wrap('http://example.com')->guessPort(); // 80
ParsedUri::wrap('http://example.com:555')->guessPort(); // 555 (returns explicitly set port if available)

// PATH
$uri->path()->toString(); // "/some/dir/file.html"
$uri->path()->segments(); // ["some", "dir", "file.html"]
$uri->path()->segment(0); // ["some"]
$uri->path()->trim(); // "some/dir/file.html"
$uri->path()->ltrim(); // "some/dir/file.html"
$uri->path()->dirname(); // "/some/dir"
$uri->path()->filename(); // "file"
$uri->path()->basename(); // "file.html"
$uri->path()->extension(); // "html"

// path helper methods
ParsedUri::wrap('/some/dir/')->path()->rtrim(); // "/some/dir"
ParsedUri::wrap('/some/dir')->path()->isAbsolute(); // true
ParsedUri::wrap('some/dir')->path()->isAbsolute(); // false
ParsedUri::wrap('/some/dir/..')->path()->absolute(); // "/some"
ParsedUri::wrap('/..')->path()->absolute(); // (throws \RuntimeException - path outside of root)
ParsedUri::wrap('/some/dir')->path()->prepend('pre/fix'); // "/pre/fix/some/dir"
ParsedUri::wrap('/some/dir')->path()->append('suf/fix'); // "/some/dir/suf/fix"
ParsedUri::wrap('/foo%20bar/baz')->path()->toString(); // "/foo bar/baz" (urldecoded)

// QUERY
$uri->query()->toString(); // "q=abc&flag=1"
$uri->query()->all(); // ["q" => "abc", "flag => "1"]
$uri->query()->has('q'); // true
$uri->query()->has('missing'); // false

$uri->query()->get('q'); // "abc"
$uri->query()->get('missing'); // (null)
$uri->query()->get('missing', 'default'); // "default"
$uri->query()->get('missing', new \Exception()); // (throws passed \Exception)

$uri->query()->getBool('flag'); // true
$uri->query()->getBool('missing'); // false
$uri->query()->getBool('missing', true); // true
$uri->query()->getBool('missing', new \Exception()); // (throws passed \Exception)

$uri->query()->getInt('flag'); // 1
$uri->query()->getInt('missing'); // 0
$uri->query()->getInt('missing', 5); // 5
$uri->query()->getInt('missing', new \Exception()); // (throws passed \Exception)

// FRAGMENT
$uri->fragment(); // "test"

ParsedUri::wrap('http://example.com')->fragment(); // (null)
ParsedUri::wrap('http://example.com#frag%20ment')->fragment(); // "frag ment" (urldecoded)

操作URI

注意Zenstruck\Uri\ParsedUri是一个不可变对象,因此任何操作都会生成一个新实例。

use Zenstruck\Uri\ParsedUri;

// URI used for the following examples
$uri = ParsedUri::wrap('https://user:[email protected]/path?q=abc&flag=1#test');

// SCHEME
$uri->withScheme('http')->toString(); // "http://user:[email protected]/path?q=abc&flag=1#test"
$uri->withoutScheme()->toString(); // "//user:[email protected]/path?q=abc&flag=1#test"

// HOST
$uri->withHost('localhost')->toString(); // "https://user:pass@localhost/path?q=abc&flag=1#test"
$uri->withoutHost()->toString(); // "https:/path?q=abc&flag=1#test" (removes username/password/port as well)

// USER
$uri->withUsername('[email protected]')->toString(); // "https://foo%40bar.com:[email protected]/path?q=abc&flag=1#test" (urlencoded)
$uri->withoutUsername()->toString(); // "https://example.com/path?q=abc&flag=1#test" (removes password as well)

// PASSWORD
$uri->withPassword('pass#word')->toString(); // "https://user:pass%[email protected]/path?q=abc&flag=1#test" (urlencoded)
$uri->withoutPassword()->toString(); // "https://[email protected]/path?q=abc&flag=1#test"

// PORT
$uri->withPort(555)->toString(); // "https://user:[email protected]:555/path?q=abc&flag=1#test"
ParsedUri::new('http://example.com:22')->withoutPort()->toString(); // "http://example.com"

// PATH
$uri->withPath('/replace')->toString(); // "https://user:[email protected]/replace?q=abc&flag=1#test"
$uri->withoutPath()->toString(); // "https://user:[email protected]?q=abc&flag=1#test"
$uri->prependPath('/prefix')->toString(); // "https://user:[email protected]/prefix/path?q=abc&flag=1#test"
$uri->appendPath('/suffix')->toString(); // "https://user:[email protected]/path/suffix?q=abc&flag=1#test"

// QUERY
$uri->withQuery(['foo' => 'bar'])->toString(); // "https://user:[email protected]/path?foo=bar#test"
$uri->withQueryParam('foo', 'bar')->toString(); // "https://user:[email protected]/path?q=abc&flag=1&foo=bar#test"
$uri->withoutQuery()->toString(); // "https://user:[email protected]/path#test"
$uri->withoutQueryParams('q', 'missing')->toString(); // "https://user:[email protected]/path?flag=1#test"
$uri->withOnlyQueryParams('q', 'missing')->toString(); // "https://user:[email protected]/path?q=abc#test"

// FRAGMENT
$uri->withFragment('frag ment')->toString(); // "https://user:[email protected]/path?q=abc&flag=1#frag%20ment" (urlencoded)
$uri->withoutFragment()->toString(); // "https://user:[email protected]/path?q=abc&flag=1"

// URI Builder
ParsedUri::new()
    ->withHost('example.com')
    ->withScheme('https')
    ->withPath('/path')
    // ...
    ->toString() // "https://example.com/path"
;

签名URI

注意:需要symfony/http-kernel来签名和验证URI,请使用命令composer require symfony/http-kernel安装。

您可以为URI签名

$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');

(string) $uri->sign('a secret'); // "https://example.com/some/path?_hash=..."

临时URI

创建一个过期的签名URI

$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');

(string) $uri->sign('a secret')->expires(new \DateTime('tomorrow')); // "https://example.com/some/path?_expires=...&_hash=..."

// # of seconds
(string) $uri->sign('a secret')->expires(3600); // "https://example.com/some/path?_expires=...&_hash=..."

// date string
(string) $uri->sign('a secret')->expires('+30 minutes'); // "https://example.com/some/path?_expires=...&_hash=..."

单次使用URI

这些URI使用一个令牌生成,该令牌应该在URI被使用后改变

注意:确定此令牌取决于上下文。该值必须在令牌成功使用后改变,否则它仍然有效。

$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');

(string) $uri->sign('a secret')->singleUse('some-token'); // "https://example.com/some/path?_token=...&_hash=..."

注意:首先使用此令牌对URL进行哈希,然后使用密钥再次进行哈希,以确保未对其进行篡改。

签名URI构建器

调用Zenstruck\Uri\ParsedUri::sign()返回一个Zenstruck\Uri\Signed\Builder对象,可用于创建单次使用和临时URI。

$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');

$builder = $uri->sign('a secret'); // Zenstruck\Uri\Signed\Builder

// create a single-use, temporary uri
$builder = $uri->sign('a secret')
    ->singleUse('some-token')
    ->expires('+30 minutes')
;

(string) $builder; // "https://example.com/some/path?_expires=...&_token=...&_hash=..."

注意Zenstruck\Uri\Signed\Builder是不可变对象,因此任何操作都会生成一个新实例。

验证

要验证签名URI,创建一个Zenstruck\Uri\ParsedUri实例,并调用isVerified()以获取true/false或调用verify()以抛出特定异常。

use Zenstruck\Uri\ParsedUri;
use Zenstruck\Uri\Signed\Exception\InvalidSignature;
use Zenstruck\Uri\Signed\Exception\ExpiredUri;
use Zenstruck\Uri\Signed\Exception\VerificationFailed;

$signedUri = ParsedUri::wrap('http://example.com/some/path?_hash=...');

$signedUri->isVerified('a secret'); // true/false

try {
    $signedUri->verify('a secret');
} catch (VerificationFailed $e) {
    $e::REASON; // ie "Invalid signature."
    $e->uri(); // \Zenstruck\Uri
}

// catch specific exceptions
try {
    $signedUri->verify('a secret');
} catch (InvalidSignature $e) {
    $e::REASON; // "Invalid signature."
    $e->uri(); // \Zenstruck\Uri
} catch (ExpiredUri $e) {
    $e::REASON; // "URI has expired."
    $e->uri(); // \Zenstruck\Uri
    $e->expiredAt(); // \DateTimeImmutable
}

单次使用验证

要验证单次使用URI,需要将令牌传递给验证方法。

use Zenstruck\Uri\Signed\Exception\InvalidSignature;
use Zenstruck\Uri\Signed\Exception\ExpiredUri;
use Zenstruck\Uri\Signed\Exception\UriAlreadyUsed;

/** @var \Zenstruck\Uri\ParsedUri $uri */

$uri->isVerified('a secret', 'some token'); // true/false

// catch specific exceptions
try {
    $uri->verify('a secret', 'some token');
} catch (InvalidSignature $e) {
    $e::REASON; // "Invalid signature."
    $e->uri(); // \Zenstruck\Uri
} catch (ExpiredUri $e) {
    $e::REASON; // "URI has expired."
    $e->uri(); // \Zenstruck\Uri
    $e->expiredAt(); // \DateTimeImmutable
} catch (UriAlreadyUsed $e) {
    $e::REASON; // "URI has already been used."
    $e->uri(); // \Zenstruck\Uri
}

签名URI

Zenstruck\Uri\Signed.Builder::create()Zenstruck\Uri\ParsedUri::verify()都返回一个实现Zenstruck\Uri并具有一些有用方法的Zenstruck\Uri\SignedUri对象。

注意Zenstruck\Uri\SignedUri始终被视为已验证,并且不能被操作。

$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');

// create from the builder
$signedUri = $uri->sign('a secret')
    ->singleUse('a token')
    ->expires('tomorrow')
    ->create()
; // Zenstruck\Uri\SignedUri

// create from verify
$signedUri = $uri->verify('a secret'); // Zenstruck\Uri\SignedUri

$signedUri->isSingleUse(); // true
$signedUri->isTemporary(); // true
$signedUri->expiresAt(); // \DateTimeImmutable

// implements Zenstruck\Uri
$signedUri->query(); // Zenstruck\Uri\Query

UriLink

提供了PSR-13链接实现,包括

  • Zenstruck\Uri\Link\UriLink(实现了Psr\Link\LinkInterfaceZenstruck\Uri)。
  • Zenstruck\Uri\Link\UriLinkProvider(实现了Psr\Link\LinkProviderInterface并提供Zenstruck\Uri\Link\UriLink)。

TemplateUri

注意:使用 rize/uri-template 是必需的,以便使用 TemplateUri - composer require rize/uri-template

Zenstruck\Uri\TemplateUri 允许创建/操作 RFC 6570 uri 模板,并实现了 Zenstruck\Uri

use Zenstruck\Uri\TemplateUri;

// Expand
$uri = TemplateUri::expand('/repos/{owner}/{repo}', ['owner' => 'kbond', 'repo' => 'foundry']);

(string) $uri; // "/repos/kbond/foundry"
$uri->template(); // "/repos/{owner}/{repo}"
$uri->parameters()->all(); // ['owner' => 'kbond', 'repo' => 'foundry']

// Extract
$uri = TemplateUri::extract('/repos/{owner}/{repo}', '/repos/kbond/foundry');

(string) $uri; // "/repos/kbond/foundry"
$uri->template(); // "/repos/{owner}/{repo}"
$uri->parameters()->all(); // ['owner' => 'kbond', 'repo' => 'foundry']

Mailto

注意Zenstruck\Uri\Mailto 是一个不可变对象,因此任何操作都会产生一个新的实例。

use Zenstruck\Uri\Mailto;

// Build
$mailto = Mailto::wrap('[email protected]')
    ->addTo('[email protected]', 'Jane')
    ->addCc('[email protected]')
    ->addBcc('[email protected]')
    ->withSubject('my subject')
    ->withBody('some body')
    ->toString() // "mailto:kevin%40example.com%2CJane%20%3Cjane%40example.com%3E?cc=ryan%40example.com&bcc=wouter%40example.com&subject=my%20subject&body=some%20body"
;

// Parse/Read
$mailto = Mailto::new('mailto:kevin%40example.com%2CJane%20%3Cjane%40example.com%3E?cc=ryan%40example.com&bcc=wouter%40example.com&subject=my%20subject&body=some%20body');

$mailto->to(); // ["[email protected]", "Jane <[email protected]>"]
$mailto->cc(); // ["[email protected]"]
$mailto->bcc(); // ["[email protected]"]
$mailto->subject(); // "my subject"
$mailto->body(); // "my body"

Twig扩展

包含了一个提供 urimailto 过滤器和函数的 twig 扩展。

手动激活

/* @var \Twig\Environment $twig */

$twig->addExtension(new \Zenstruck\Uri\Bridge\Twig\UriExtension());

Symfony 全栈激活

# config/packages/zenstruck_uri.yaml

Zenstruck\Uri\Bridge\Twig\UriExtension: ~

# If not using auto-configuration:
Zenstruck\Uri\Bridge\Twig\UriExtension:
    tag: twig.extension

用法

{# Filters: #}
{{ 'https://example.com'|uri.withPath('some/path').withQueryParam('q', 'term') }} {# https://example.com/some/path?q=term #}
{{ '[email protected]'|mailto.withSubject('my subject') }} {# mailto:kevin%40example.com?subject=my%20subject #}

{# Functions: #}
{{ uri().withScheme('https').withHost('example.com') }} {# https://example.com #}
{{ mailto().withTo('[email protected]').withSubject('my subject') }} {# mailto:kevin%40example.com?subject=my%20subject #}