zenstruck/uri
Immutable, fluent URI/URL value objects for PHP with easy parsing, building, and modification of URIs. Create, normalize, and manipulate components (scheme, host, path, query, fragment) safely, with helpers for encoding and query params.
Object-oriented wrapper/manipulator for parse_url with the following features:
Scheme, Host, Path, Query), each with their own
set of features.mailto: URIs.This library is meant as a wrapper for PHP's parse_url function only and does not
conform to any URI-related PSR or RFC. If you need this,
league/uri would be a better choice.
composer require zenstruck/uri
use Zenstruck\Uri\ParsedUri;
// wrap a uri (this URI will be used for many of the samples below)
$uri = ParsedUri::wrap('https://username:password@example.com/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%23word@example.com')->username(); // foo@bar.com (urldecoded)
ParsedUri::wrap('http://foo%40bar.com:pass%23word@example.com')->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)
Note:
Zenstruck\Uri\ParsedUriis an immutable object so any manipulations results in a new instance.
use Zenstruck\Uri\ParsedUri;
// URI used for the following examples
$uri = ParsedUri::wrap('https://user:pass@example.com/path?q=abc&flag=1#test');
// SCHEME
$uri->withScheme('http')->toString(); // "http://user:pass@example.com/path?q=abc&flag=1#test"
$uri->withoutScheme()->toString(); // "//user:pass@example.com/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('foo@bar.com')->toString(); // "https://foo%40bar.com:pass@example.com/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%23word@example.com/path?q=abc&flag=1#test" (urlencoded)
$uri->withoutPassword()->toString(); // "https://user@example.com/path?q=abc&flag=1#test"
// PORT
$uri->withPort(555)->toString(); // "https://user:pass@example.com: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:pass@example.com/replace?q=abc&flag=1#test"
$uri->withoutPath()->toString(); // "https://user:pass@example.com?q=abc&flag=1#test"
$uri->prependPath('/prefix')->toString(); // "https://user:pass@example.com/prefix/path?q=abc&flag=1#test"
$uri->appendPath('/suffix')->toString(); // "https://user:pass@example.com/path/suffix?q=abc&flag=1#test"
// QUERY
$uri->withQuery(['foo' => 'bar'])->toString(); // "https://user:pass@example.com/path?foo=bar#test"
$uri->withQueryParam('foo', 'bar')->toString(); // "https://user:pass@example.com/path?q=abc&flag=1&foo=bar#test"
$uri->withoutQuery()->toString(); // "https://user:pass@example.com/path#test"
$uri->withoutQueryParams('q', 'missing')->toString(); // "https://user:pass@example.com/path?flag=1#test"
$uri->withOnlyQueryParams('q', 'missing')->toString(); // "https://user:pass@example.com/path?q=abc#test"
// FRAGMENT
$uri->withFragment('frag ment')->toString(); // "https://user:pass@example.com/path?q=abc&flag=1#frag%20ment" (urlencoded)
$uri->withoutFragment()->toString(); // "https://user:pass@example.com/path?q=abc&flag=1"
// URI Builder
ParsedUri::new()
->withHost('example.com')
->withScheme('https')
->withPath('/path')
// ...
->toString() // "https://example.com/path"
;
Note:
symfony/http-kernelis required to sign and verify URIscomposer require symfony/http-kernel.
You can sign a URI:
$uri = Zenstruck\Uri\ParsedUri::wrap('https://example.com/some/path');
(string) $uri->sign('a secret'); // "https://example.com/some/path?_hash=..."
Make an expiring signed 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=..."
These URIs are generated with a token that should change once the URI has been used.
Note: It is up to you to determine this token and depends on the context. This value MUST change after the token is successfully used, else it will still be valid.
$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=..."
Note: The URL is first hashed with this token, then hashed again with secret to ensure it hasn't been tampered with.
Calling Zenstruck\Uri\ParsedUri::sign() returns a Zenstruck\Uri\Signed\Builder object that can be used to
create single-use and temporary URIs.
$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=..."
Note:
Zenstruck\Uri\Signed\Builderis immutable objects so any manipulations results in a new instance.
To verify a signed URI, create an instance of Zenstruck\Uri\ParsedUri and call isVerified() to
get true/false or verify() to throw specific exceptions:
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
}
For validating single-use URIs, you need to pass a token to the verify methods:
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
}
SignedUriZenstruck\Uri\Signed\Builder::create() and Zenstruck\Uri\ParsedUri::verify() both return a
Zenstruck\Uri\SignedUri object that implements Zenstruck\Uri and has some helpful methods.
Note:
Zenstruck\Uri\SignedUriis always considered verified and cannot be manipulated.
$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
UriLinkA PSR-13 Link implementation is provided with:
Zenstruck\Uri\Link\UriLink (implements both Psr\Link\LinkInterface and Zenstruck\Uri).Zenstruck\Uri\Link\UriLinkProvider (implements Psr\Link\LinkProviderInterface and
provides Zenstruck\Uri\Link\UriLink's).TemplateUriNote:
rize/uri-templateis required to useTemplateUri-composer require rize/uri-template.
Zenstruck\Uri\TemplateUri allows creating/manipulating RFC 6570
uri templates and implements 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']
MailtoNote:
Zenstruck\Uri\Mailtois an immutable object so any manipulations results in a new instance.
use Zenstruck\Uri\Mailto;
// Build
$mailto = Mailto::wrap('kevin@example.com')
->addTo('jane@example.com', 'Jane')
->addCc('ryan@example.com')
->addBcc('wouter@example.com')
->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(); // ["kevin@example.com", "Jane <jane@example.com>"]
$mailto->cc(); // ["ryan@example.com"]
$mailto->bcc(); // ["wouter@example.com"]
$mailto->subject(); // "my subject"
$mailto->body(); // "my body"
A twig extension providing uri, mailto filters and
functions is included.
/* @var \Twig\Environment $twig */
$twig->addExtension(new \Zenstruck\Uri\Bridge\Twig\UriExtension());
# 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 #}
{{ 'kevin@example.com'|mailto.withSubject('my subject') }} {# mailto:kevin%40example.com?subject=my%20subject #}
{# Functions: #}
{{ uri().withScheme('https').withHost('example.com') }} {# https://example.com #}
{{ mailto().withTo('kevin@example.com').withSubject('my subject') }} {# mailto:kevin%40example.com?subject=my%20subject #}
How can I help you explore Laravel packages today?