URI packages reloaded

Yesterday I announced that the new major version of the league URI packages were released.
Because I am not the best expert in marketing I presume that people assume that only one
package was released while in fact 3 packages were released at the same time.

New architecture

As you might notice they all share the same version number and some packages just jumped major version released. The reason behind the jump is rather simple, all 3 packages have been developed as mono-repo for more than a year so it was about time that I normalise their release cycle.

And while on the surface this is just a number change behind the scene for the past couple months I have been rewriting and re-organising the architecture of those 3 packages to ease maintenance but more important to boost the packages usage. A great example of that is the example I have shown in one of my announcement
post:

use League\Uri\BaseUri;
use League\Uri\Modifier;

$uri = "https://www.bbc.co.uk";
$relativeUri = "path/to/../../the/sky/?foo.bar";
$appendQuery = "foo.bar=tata";

$resolvedUri = BaseUri::from($uri)->resolve($relativeUri);
$newUri = Modifier::from($resolvedUri)
    ->appendQuery($appendQuery)
    ->removeLabels(1)
    ->removeTrailingSlash()
    ->getUriString();

echo $newUri;
// displays https://www.bbc.uk/the/sky?foo.bar&foo.bar=tata

It might surprise some of you but this was already possible using the previous packages
versions. The code would then look like this:

use League\Uri\Uri;
use League\Uri\UriModifier;
use League\Uri\UriResolver;

$uri = "https://www.bbc.co.uk";
$relativeUri = "path/to/../../the/sky/?foo.bar";
$appendQuery = "foo.bar=tata";

$baseUri = Uri::createFromString($uri);
$relativedUri = Uri::createFromString($relativeUri);
$resolvedUri = UriResolver::resolve($relativedUri, $baseUri);
$newUri = UriModifier::appendQuery($resolvedUri,  $appendQuery);
$newUri = UriModifier::removeLabels($newUri, 1);
$newUri = UriModifier::removeTrailingSlash($newUri);
$newUri = $newUri->__toString();

echo $newUri;
// displays https://www.bbc.uk/the/sky?foo.bar&foo.bar=tata

Apart from chaining, the main differences between those two style of code is that in v7 we are less strict on the input argument type, it should at least be a string or a Stringable object, we are still as strict on the returned value. This means that the code is smart enough to detect the input and if it is not a known URI object it will attempt to convert the input into one before performing the intended action. In contrast, with the older version, we expected the developper consuming the library to do it for us before submitting the URI the to package API.

To achieve the new behaviour I had to re-organised the packages. Before, The URI and and URI
components were fully decoupled. You could download and install each package independently.
This is no longer true, each package is now built on top of the other. The URI package
requires the URI interfaces package and the URI component package requires the URI package.
With this change it means that, for instance, the URI component Modifier class knows how to
instantiate at any given time a URI. This was not possible before.

Same code, new features

Another improvement because of the new coupling between classes is that I could re-organised the classes between packages. For instance, all the parsers (the URI parser and the Query parser) are now part of the URI Interface packages. The role of this package has evolved since its first release. It used to be a package that only contains contracts used by the other two packages, now it is also a package that exposes common tools to use when handling URI, parsers fall into that description. In addition, the new architecture gave me the opportunity to rewrite and expose other tools independent of URI packages.

use League\Uri\Idna\Converter as IdnConverter;
use League\Uri\IPv4\Converter as Ipv4Converter;

echo IdnConverter::toAscii('bébé.be')->domain(); //display "xn--bb-bjab.be"
echo Ipv4Converter::fromEnvironment()->toDecimal('192.87.125'); //display "192.87.0.125"

Those two features were already present in the codebase but the rewrite and architectural
changes enabled their exposition and improved their usage inside and outside the packages.

Avoid re-inventing the wheel

One of the most difficult thing when complying to RFC and specifications is that most of them handle
gracefully errors and mistakes. Which means that it may be hard for developers to know if something
is not the expected result or not. In this new version, I try to improve the developer experience when possible using the following pattern:

use League\Uri\UriTemplate;
$template = 'https://api.example.com/{version}/search/{term}/{?q*,limit}';

$params = [
    'term' => ['john', 'doe'],
    'q' => ['a', 'b'],
    'limit' => '10',
];

$uriTemplate = new UriTemplate($template);
echo $uriTemplate->expand($params);
// display https://api.example.com//search/john,doe/?q=a&q=b&limit=10 with missing version
// this is valid and follow the specifications

echo $uriTemplate->expandOrFail($params);
// will throw a TemplateCanNotBeExpanded exception with the following message
// Missing variables `version`

The new expandOrFail method is stricter than the RFC compliant expand method and throw if a variable is missing. It will be easier for developer to recognise the method behaviour because it is a well established signature in some PHP communities but more importantly it avoids adding extra boolean argument to a well establish method which makes the intent clearer. In summary, when possible, stricter method are provided with the orFail suffix.

Backward compatibility

What’s great about all these changes is that you will not have to rewrite your application, as the new version is fully backward compatible with the previous packages. Which all your current code is still valid with the new version even if your IDE of choice will more than likely flag the code as being deprecated.
This should give you plenty of time to upgrade your current codebase but if you are new to the package I encourage you to not look in the past and to embrace the new public API which frees you for most of the basic usage from learning how URI works and let’s you focus on your actual application logic.

While this post is not aiming at presenting all the new features, you can head over the
documentation website to get the full public API, I hope it gave you the underlying motivation for this new version which was to make the code easier to interact with and more intuitive.

Last but not least

The URI packages are open source project with a MIT License so contributions are more than welcome and will be fully credited. These contributions can be anything from reporting an issue, requesting or adding missing features or simply improving or correcting some typo on the documentation website.

2 thoughts on “URI packages reloaded

  1. I’ve run through a refactoring of code from php 7.2 to 8.2 and the following warnings have popped up:
    For league/uri-hostname-parser, you could use symfony/string.
    For league/uri-manipulations, you could use symfony/http-foundation.
    For league/uri-parser, you could use symfony/http-foundation.
    For league/uri-schemes, you could use symfony/http-foundation.
    For timetoogo/rapid-route, you could use symfony/routing.
    For phpunit/php-token-stream, you could use nikic/php-parser.
    Are there replacements for these?

    • No they are not at least not for the league package. Please refer to the documentation website to find which package to use when upgrading,

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.