joby / smol-url
A simple and lightweight extensible URL library designed for working with URLs in human-scale applications.
Installs: 130
Dependents: 2
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/joby/smol-url
Requires
- php: >=8.1
Requires (Dev)
- php: >=8.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12
This package is auto-updated.
Last update: 2026-01-17 21:34:45 UTC
README
A simple and lightweight extensible URL library designed for working with URLs in human-scale applications.
What is smolURL?
smolURL is a modern PHP library for working with URLs using an immutable, component-based architecture. Unlike built-in PHP URL functions, smolURL represents URLs as composed objects where each component (path, query, fragment, host, etc.) is its own readonly class.
Key features:
- Immutable design: All URL components are readonly and use
with*()methods to create modified copies - Type-safe: Built with PHP 8.3+ features including readonly properties and typed parameters
- Component-based: Each URL part (Path, Query, Fragment, Scheme, Host, Port, User) is a separate class
- Security-focused: Intentionally limited to HTTP/HTTPS schemes to prevent arbitrary scheme parsing
- Link resolution: Built-in support for resolving relative links (like HTML
<a>tags) - Parsing & factories: Parse strings or globals and compose URLs against a base URL
- Clean API: All components implement
Stringablefor easy conversion to strings
Installation
composer require joby/smol-url
Basic usage
Quick start
use Joby\Smol\URL\{URL, Path, Query, Scheme, Host}; $url = new URL( Path::fromString('/docs'), new Query(['q' => 'smol']), scheme: Scheme::HTTPS, host: new Host('example.com') ); echo $url; // "https://example.com/docs?q=smol"
Creating URLs
use Joby\Smol\URL\{URL, Path, Query, Fragment, Scheme, Host, Port, User}; // Simple absolute path $url = new URL(new Path(filename: 'page.html')); echo $url; // "/page.html" // Full URL with all components $url = new URL( path: new Path(['dir1', 'dir2'], 'file.php'), query: new Query(['key' => 'value']), fragment: new Fragment('section'), scheme: Scheme::HTTPS, host: new Host('example.com'), port: new Port(8080), user: new User('username', 'password') ); echo $url; // "https://username:password@example.com:8080/dir1/dir2/file.php?key=value#section" // Relative paths $url = new URL(new Path(filename: 'page.html', absolute: false)); echo $url; // "page.html"
Parsing URLs
use Joby\Smol\URL\{UrlFactory, URL}; $factory = new UrlFactory(); // From string $url = $factory->fromString('https://example.com/path?x=1#frag'); // From globals (REQUEST_URI, HOST, etc.) $current = $factory->fromGlobals(); // Merge a URL with the factory base URL $base = $factory->baseUrl(); $composed = $factory->fromUrl(new URL(path: $base->path));
Working with paths
// Create from string $path = Path::fromString('/dir1/dir2/file.php'); // Access components $path->directory; // ['dir1', 'dir2'] $path->filename; // 'file.php' $path->absolute; // true // Get directory path $path->dirname(); // "/dir1/dir2/"
Manipulating query parameters
$query = new Query(['page' => '1', 'sort' => 'name']); // Access values with type safety $page = $query->getInt('page'); // 1 $sort = $query->get('sort'); // 'name' $missing = $query->get('foo', 'bar'); // 'bar' (default) // Check for parameters $query->has('page'); // true // Require parameters (throws exception if missing) $page = $query->requireInt('page'); // Create modified copies $newQuery = $query->withArg('limit', 10); $newQuery = $query->withArgs(['page' => 2, 'limit' => 10]); $newQuery = $query->withoutArg('sort');
Modifying URLs immutably
$url = new URL( Path::fromString('/page'), new Query(['id' => '123']), scheme: Scheme::HTTP, host: new Host('example.com') ); // Create modified versions $https = $url->withScheme(Scheme::HTTPS); $newPath = $url->withPath(Path::fromString('/other')); $newQuery = $url->withQuery(new Query(['id' => '456'])); // Original URL is unchanged echo $url; // "http://example.com/page?id=123" echo $https; // "https://example.com/page?id=123"
URL query helpers
To make query edits less verbose, URL exposes helper methods that delegate to the Query object and return new URLs:
$url = new URL( Path::fromString('/page'), new Query(['a' => '1']), scheme: Scheme::HTTP, host: new Host('example.com') ); $url = $url->withArg('b', 2); // adds/updates a single arg $url = $url->withArgs(['c' => true]); // adds/updates multiple args $url = $url->withoutArg('a'); // removes one arg $url = $url->withoutArgs(['b', 'c']); // removes multiple args
Permissive with*() inputs
Several with*() methods accept additional input types for convenience:
$url = new URL(new Path(absolute: true)); $url = $url->withScheme('https'); // string or Stringable $url = $url->withHost('example.com'); // string or Stringable $url = $url->withPort(8080); // int $url = $url->withFragment('section'); // string or Stringable $url = $url->withQuery(['a' => '1']); // array $url = $url->withPath('/docs'); // string or Stringable
Resolving relative links
URLs include a withLinkStringApplied() method that allows updating URLs using a variety of relative URL strings, including relative paths, fragments, and both partial and full query string updates.
$base = new URL( Path::fromString('/dir1/dir2/page.html'), new Query(['a' => '1']) ); // Apply relative links (like HTML <a href="...">) $url = $base->withLinkStringApplied('other.html'); echo $url; // "/dir1/dir2/other.html" $url = $base->withLinkStringApplied('../file.html'); echo $url; // "/dir1/file.html" $url = $base->withLinkStringApplied('?b=2'); echo $url; // "/dir1/dir2/page.html?b=2" $url = $base->withLinkStringApplied('&b=2'); echo $url; // "/dir1/dir2/page.html?a=1&b=2" $url = $base->withLinkStringApplied('#section'); echo $url; // "/dir1/dir2/page.html#section"
Validation and encoding
- Host validation:
Hostvalidates IP addresses and domain names. - Path normalization:
Pathresolves.and..segments and rejects.or..filenames. - Encoding:
PathandFragmentencode their values for safe URL output;Queryuseshttp_build_query()for encoding.
Error handling
Invalid inputs throw URLException or QueryException depending on the component. For example, invalid host names, query value types, or missing required query keys will raise exceptions.
Limitations
- HTTP/HTTPS only: The library intentionally only supports HTTP and HTTPS schemes. This is a security feature to prevent parsing of arbitrary schemes like
javascript:,data:, etc. (It does strictly allow using any backed enum as the scheme, so you could extend it to support more schemes if you like.) - No query parameter arrays: Query parameters are limited to scalar types (strings, integers, floats, booleans). Arrays and objects are not supported to keep the implementation simple and focused.
- Immutable only: All components are readonly and immutable. You cannot modify a URL or its components in place; you must use the
with*()methods to create new instances with your changes.
Requirements
Fully tested on PHP 8.3+, static analysis for PHP 8.1+.
License
MIT License - See LICENSE file for details.