PSR-7 URI Implementation
A PSR-7 URI that also works as a connection string: the object behind the mysql://, amqp:// or s3:// URIs of the ByJG components.
Why
Connection strings look like URIs, but a strict PSR-7 implementation mangles them: it cannot
represent a host and port without a scheme (kafka:9092), and it percent-encodes the path, which a
driver reads as a name (a file, a database). Passwords with special characters need encoding by hand.
This URI keeps what a connection string needs, and adds what PSR-7 leaves out:
$uri = Uri::getInstance('mysql://user:p%40ss@db:3306/shop?charset=utf8');
$uri->getPassword(); // "p@ss", decoded
$uri->getQueryPart('charset'); // "utf8"
$uri->withQueryKeyValue('timeout', '5');
The same class reads the connection strings of the ByJG components (databases, NoSQL stores, message queues, mail, HTTP), so a connection is always configured the same way.
Features
- Implements PSR-7 UriInterface, with deliberate differences for connection strings (see PSR-7 differences)
- Implements the PSR-17
UriFactoryInterface(UriFactory) - Includes additional utility methods via CustomUriInterface
- Supports RFC3986 URI specification
- Type-safe implementation with PHP 8 attributes and strict typing
- Immutable class design pattern
PSR-7 differences
The URI is also used for connection strings (mysql://host:3306/db, h:9092), so some PSR-7 rules are not followed:
- A URI without a scheme is a host, not a relative path, and is written without
//.new Uri('h:9092')is read as hosthand port9092and written back ash:9092, not//h:9092;foo/baris hostfooand path/bar. A URI with a scheme always has//after it, even without a host (sqlite:///tmp/db). getPath()returns the path as given, not percent-encoded, since the drivers read it as a name (sqlite:///tmp/my db.sqlitehas the path/tmp/my db.sqlite).withUserInfo()takes the user and the password as they are, not encoded, so a password with%41keeps it.
The standard port of http (80) and https (443) is left out, as PSR-7 requires: https://host:443 has the port
null and is written https://host. The port of any other scheme is kept (mysql://db:3306).
URI Encoding Behavior
PSR-7 requires URI compliant with RFC3986. This means the URI output will always be URL encoded. This applies to both creating a new instance and the string representation.
Examples:
// Creating a URI with special characters in the password
$uri = \ByJG\Util\Uri::getInstance("https://user:pa&@host");
print((string)$uri); // Will print "https://user:pa%26@host"
// Creating a URI with already encoded characters
$uri = \ByJG\Util\Uri::getInstance("https://user:pa%26@host");
print((string)$uri); // Will print "https://user:pa%26@host"
// Using withUserInfo with unencoded password
$uri = \ByJG\Util\Uri::getInstance("https://host")
->withUserInfo("user", "pa&");
print((string)$uri); // Will print "https://user:pa%26@host"
// Using withUserInfo with already encoded password
$uri = \ByJG\Util\Uri::getInstance("https://host")
->withUserInfo("user", "pa%26");
print((string)$uri); // Will print "https://user:pa%2526@host"
// The username is decoded by getUsername() and encoded in the string form, like the password
print((string)$uri); // Will print "https://email%40host.com:pass@host"
Additional Methods
This implementation extends PSR-7 UriInterface with the following additional methods through \ByJG\Util\CustomUriInterface:
| Method | Description |
|---|---|
getUsername(): ?string | Get the decoded username of the URI |
getPassword(): ?string | Get the decoded password of the URI |
getQueryPart(string $key): ?string | Get a specific query parameter by key |
getQueryParts(string $key): array | Get every value of a query parameter |
withQueryKeyValue(string $key, string $value, bool $isEncoded = false): self | Add or update a query parameter |
hasQueryKey(string $key): bool | Check if a query parameter exists |
Static Factory Methods
The class provides convenient static factory methods:
// Create from string
$uri = Uri::getInstance("https://example.com/path?query=value#fragment");
// Create from another UriInterface
$uri2 = Uri::getInstance($uri);
Code that receives a PSR-17 UriFactoryInterface gets the same Uri from UriFactory:
$uri = (new \ByJG\Util\UriFactory())->createUri("https://example.com/path");
Documentation
Installation
composer require "byjg/uri"
Running Tests
vendor/bin/phpunit
The PSR-7 and PSR-17 conformance suites, the ones other implementations run
(php-http/psr7-integration-tests and
http-interop/http-factory-tests), are part of
the run. The tests of the PSR-7 differences are skipped with the reason
(tests/Conformance/UriIntegrationTest.php).
vendor/bin/phpunit --testsuite Conformance