zenstruck / uri
对象包装器/操作器,用于解析URL,并具有额外功能。
Requires
- php: >=8.0
- psr/link: ^1.0|^2.0
Requires (Dev)
- phpstan/phpstan: ^1.4
- phpunit/phpunit: ^9.5
- rize/uri-template: ^0.3.5
- symfony/browser-kit: ^5.4|^6.0|^7.0
- symfony/framework-bundle: ^5.4|^6.0|^7.0
- symfony/phpunit-bridge: ^6.1|^7.0
- symfony/twig-bundle: ^5.4|^6.0|^7.0
- symfony/web-link: ^5.4|^6.0|^7.0
Suggests
- rize/uri-template: To use TemplateUri
- symfony/http-kernel: To create and verify signed URIs
Provides
- psr/link-implementation: 1.0|2.0
README
parse_url
的对象包装器/操作器,具有以下功能
- 将URI部分(
Schema
、Host
、Path
、Query
)作为对象读取,每个对象都有其自己的功能集。 - 操作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\LinkInterface
和Zenstruck\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扩展
包含了一个提供 uri
、mailto
过滤器和函数的 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 #}