PHPackages                             zenstruck/uri - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. zenstruck/uri

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

zenstruck/uri
=============

Object-oriented wrapper/manipulator for parse\_url with additional features.

v2.4.0(5mo ago)1767.8k—0.4%1[1 issues](https://github.com/zenstruck/uri/issues)5MITPHPPHP &gt;=8.0CI failing

Since Jan 9Pushed 3mo ago2 watchersCompare

[ Source](https://github.com/zenstruck/uri)[ Packagist](https://packagist.org/packages/zenstruck/uri)[ Docs](https://github.com/zenstruck/uri)[ GitHub Sponsors](https://github.com/kbond)[ GitHub Sponsors](https://github.com/nikophil)[ RSS](/packages/zenstruck-uri/feed)WikiDiscussions 2.x Synced 1mo ago

READMEChangelog (10)Dependencies (10)Versions (15)Used By (5)

zenstruck/uri
=============

[](#zenstruckuri)

[![CI](https://github.com/zenstruck/uri/actions/workflows/ci.yml/badge.svg)](https://github.com/zenstruck/uri/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/1c974602899a269213653b2daf320be5c8e5cc3832eb677642ee0b8648526c1c/68747470733a2f2f636f6465636f762e696f2f67682f7a656e73747275636b2f7572692f6272616e63682f322e782f67726170682f62616467652e7376673f746f6b656e3d33335147335a41334730)](https://codecov.io/gh/zenstruck/uri)

Object-oriented wrapper/manipulator for `parse_url` with the following features:

- Read URI *parts* as objects (`Scheme`, `Host`, `Path`, `Query`), each with their own set of features.
- Manipulate URI parts or build URI's using a fluent builder API.
- [Sign and verify](#signed-uris) URI's and make them temporary and/or single-use.
- [Mailto object](#mailto) to help with reading/manipulating `mailto:` URIs.
- [URI Template](#templateuri) ([RFC 6570](http://tools.ietf.org/html/rfc6570)) support.
- [PSR-13 Link](#urilink) implementation/bridge.
- [Twig Extension](#twig-extension).

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](https://uri.thephpleague.com/) would be a better choice.

Installation
------------

[](#installation)

```
composer require zenstruck/uri
```

Parsing/Reading URIs
--------------------

[](#parsingreading-uris)

```
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)
```

Manipulating URIs
-----------------

[](#manipulating-uris)

> **Note**: `Zenstruck\Uri\ParsedUri` is 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"
;
```

Signed URIs
-----------

[](#signed-uris)

> **Note**: `symfony/http-kernel` is required to sign and verify URIs `composer 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=..."
```

### Temporary URIs

[](#temporary-uris)

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=..."
```

### Single-Use URIs

[](#single-use-uris)

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.

### Signed URI Builder

[](#signed-uri-builder)

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\Builder` is immutable objects so any manipulations results in a new instance.

### Verification

[](#verification)

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
}
```

#### Single-Use Verification

[](#single-use-verification)

For validating [single-use URIs](#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
}
```

### `SignedUri`

[](#signeduri)

`Zenstruck\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\SignedUri` is 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
```

`UriLink`
---------

[](#urilink)

A [PSR-13 Link](https://www.php-fig.org/psr/psr-13/) 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).

`TemplateUri`
-------------

[](#templateuri)

> **Note**: `rize/uri-template` is required to use `TemplateUri` - `composer require rize/uri-template`.

`Zenstruck\Uri\TemplateUri` allows creating/manipulating [RFC 6570](http://tools.ietf.org/html/rfc6570)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']
```

`Mailto`
--------

[](#mailto)

> **Note**: `Zenstruck\Uri\Mailto` is 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 "]
$mailto->cc(); // ["ryan@example.com"]
$mailto->bcc(); // ["wouter@example.com"]
$mailto->subject(); // "my subject"
$mailto->body(); // "my body"
```

Twig Extension
--------------

[](#twig-extension)

A [twig](https://twig.symfony.com/) extension providing `uri`, `mailto` filters and functions is included.

### Manual activation

[](#manual-activation)

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

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

### Symfony full-stack activation

[](#symfony-full-stack-activation)

```
# config/packages/zenstruck_uri.yaml

Zenstruck\Uri\Bridge\Twig\UriExtension: ~

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

### Usage

[](#usage)

```
{# 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 #}
```

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance76

Regular maintenance activity

Popularity39

Limited adoption so far

Community15

Small or concentrated contributor base

Maturity62

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~107 days

Recently: every ~298 days

Total

15

Last Release

92d ago

Major Versions

v0.1.1 → v1.0.02022-06-01

v1.3.0 → v2.0.02022-11-10

PHP version history (2 changes)v0.1.0PHP &gt;=7.4

v2.0.0PHP &gt;=8.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/707369cc916e0ea1aacbf077dcba464f611cef879f024d8944311a54a15224b3?d=identicon)[kbond](/maintainers/kbond)

---

Top Contributors

[![kbond](https://avatars.githubusercontent.com/u/127811?v=4)](https://github.com/kbond "kbond (74 commits)")

---

Tags

urluriparsersignedmailto

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zenstruck-uri/health.svg)

```
[![Health](https://phpackages.com/badges/zenstruck-uri/health.svg)](https://phpackages.com/packages/zenstruck-uri)
```

###  Alternatives

[nikic/php-parser

A PHP parser written in PHP

17.4k902.6M1.8k](/packages/nikic-php-parser)[doctrine/lexer

PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.

11.2k910.8M118](/packages/doctrine-lexer)[masterminds/html5

An HTML5 parser and serializer.

1.8k242.8M229](/packages/masterminds-html5)[matomo/device-detector

The Universal Device Detection library, that parses User Agents and detects devices (desktop, tablet, mobile, tv, cars, console, etc.), clients (browsers, media players, mobile apps, feed readers, libraries, etc), operating systems, devices, brands and models.

3.5k23.5M111](/packages/matomo-device-detector)[vstelmakh/url-highlight

Library to parse urls from string input

102849.1k9](/packages/vstelmakh-url-highlight)[crwlr/url

Swiss Army knife for URLs.

11073.3k3](/packages/crwlr-url)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
