— nyamsprod (@nyamsprod) January 5, 2015
As I am working on the next version of the League\URL package, I reached a point where a BC break must be done around the URLs classes. In the current stable version (version 3), the URLs can be treated as mutable or immutable objects. This means that depending on your needs you may choose to represent your URLs one way or another. To keep the package tight and simple my approach was to introduce an
URLInterface interface that both classes could share. This simple approach introduced a major problem that need to be address in the next version.
To understand the issue let’s illustrate it with the following code:
First, let’s write a simple code using the mutable version of the URL class.
$url = League\Url\Url::createFromUrl('http://example.com'); $new_url = $url->setQuery(['foo' => 'bar']); $url->sameValueAs($new_url); //return true; $url === $new_url; //return true; //$new_url is a reference to the $url, the $url displays 'http://example.com?foo=bar'
Now, let’s rewrite the same code using the immutable version.
$url = League\Url\UrlImmutable::createFromUrl('http://example.com'); $new_url = $url->setQuery(['foo' => 'bar']); $url->sameValueAs($new_url); //return false; echo $url; //still displays 'http://example.com' echo $new_url; //displays 'http://example.com?foo=bar'
The root of the problem is that while both code follow the same interface the end result is quiet different. So only relying on the
URLInterface interface is not enough to represent how update made to an URL are applied. This may even break someone code if the developer typehint on the interface only, which is what the interface was supposed to be used for.
A way to reduce this issue is to:
- rename modifying methods in the immutable version to distinguish them from the mulable version. To this end, I have replace by
setprefix on all the modifying methods of the immutable version.
- remove the fluent interface from the mutable URL version.
- reduce the
URLInterfaceto shared getter methods so that both class can continue to follow the same interface.
Unfortunately, this does not solve the issue. As seen with the following snippets, the issue is jut less visible:
Let’s write another simple code
$url = League\Url\Url::createFromUrl('http://example.com'); $query = $url->getQuery()->set(['foo' => 'bar']); $query->sameValueAs($url->getQuery()); //return true $query === $url->getQuery(); //$query is a reference to the private $url->query method //$url as changed to http://example.com?foo=bar
Now let’s rewrite the same code using the immutable version.
$url = League\Url\UrlImmutable::createFromUrl('http://example.com'); $query = $url->getQuery()->set(['foo' => 'bar']); $query->sameValueAs($url->getQuery()); //return false //$query is a clone of the private method $url->query; //$query as changed and is not equals $url->query anymore
The only way out of this is to simply drop one of the URLs class. And as I write these lines I’m leaning toward removing the mutable URL class, why ?
URLs are string representation of a location:
- If you only change one character from one of its components you gain a whole new URL.
- To be considered valid, an URL need to have all its components valid.
- The URLs components may be loosely independent from each other but their meaning/representation can only be fully understood when being part of a complete URL.
So URLs are good candidate to being immutable value object. Treating URLs as being immutable does not remove any of the current features expose by the package they still all remains the same, but you gain clarity of intent whenever you access/modify the URL. The only drawbacks that I could find so far are:
- You may need more lines of code to update an URL object. This issue may be resolve by using a proxy of some kind for the most obvious operations.
- Developers may think of the new URL class as being a simple builder.
$url = League\Url\UrlImmutable::createFromUrl('http://example.com'); $host = $url->getHost(); $host->prepend('subdomain'); $new_url = $url ->withHost($host) ->withQuery(['foo' => 'bar']) ->withPort(81); echo $new_url; //displays http://subdomain.example.com:81?foo=bar; echo $url; // displays 'http://example.com'
So what do you think ? Your feedbacks are welcomed.