On Mon, Mar 10, 2025, at 5:51 PM, Máté Kocsis wrote: > I'm sure that people will find their use-cases to subclass all these > new classes, including the WHATWG implementation. As Nicolas mentioned, > his main use-case is minly adding convenience and new factory methods > that don't specifically need all methods to be reimplemented. > > While I share your opinion that leaving the URI classes open for > extension is somewhat risky and it's difficult to assess its impacts > right now, I can also > sympathise with what Nicolas wrote in a later message > (https://externals.io/message/123997#126489): that we shouldn't close > the door for the public from > using interchangeable implementations. > > I know that going final without any interfaces is the most "convenient" > for the PHP project itself, because the solution has much less BC > surface to maintain, > so we are relatively free and safe to make future changes. This is > useful for an API in its early days that is huge like this. Besides the > interests of the maintainers, > we should also take two important things into account: > > - Heterogeneous use-cases: it's out of question that the current API > won't fit all use-cases, especially because we have already identified > some followup tasks > that should be implemented (see "Future Scope" section in the RFC). > - Interoperability: Since URI handling is a very widespread problem, > many people and libraries will start to use the new extension once it's > available. But because > of the above reason, many of them want to use their own abstraction, > and that's exactly why a common ground is needed: there's simply not a > single right possible > implementation - everyone has their own, given the complexity of the > topic. > > So we should try to be considerate about these factors by some way or > another. So far, we have four options: > > - Making the classes open for extension: this solution has acknowledged > technical challenges > (https://github.com/php/php-src/pull/14461#discussion_r1847316607), > and it limits our possibilities of adding changes the most, but users > can effectively add any behavior that they need. Of course, they are > free to introduce bugs and > spec-incompatible behavior into their own implementation, but none of > the other solutions could prevent such bugs either, since people will > write their custom code > wherever they can: if they can't have it in a child class, then they > will have in MyUri, or in UriHelper, or just in a 200 lines long > function. > > Being able to extend the built-in classes also means that child classes > can use the behavior of their parent by default - there's no need to > create wrapper > classes around the built-in ones (aka using composition), that is a > tedious task to implement, and also which would incur some performance > penalty because of the > extra method calls. > > - Making the classes open for extension, but making some methods final: > same benefits as above, without the said technical challenges - in > theory. I am currently > trying to figure out if there is a combination of methods that could be > made final so that the known challenges become impossible to be > triggered - although I haven't > managed to come up with a sensible solution yet. > > - Making the classes final: It avoids some edge-cases for the built-in > classes (the uninitialized state most prominently), while it leaves the > most room for making future > changes. Projects that may want to ship their own abstractions for the > two built-in classes can use composition to create their own URI > implementations. > They can instantiate these implementations however they want to (i.e. > $myUri = new MyUri($uri)). If they need to pass an URI to other > libraries then they could extract > the wrapped built-in class (i.e. $myUri->getUri()). > > On the flipside, backporting methods added in future PHP versions (aka > polyfills) will become impossible to implement for URIs according to my > knowledge, as well as mocking > in PHPUnit will also be a lost feature (I'm not sure if it's a good or > a bad thing, but it may be worth to point out). > > Also, the current built-in implementations may have alternative > implementations that couldn't be used instead of them. For example, the > ADA URL library (which is mentioned > in the RFC) also implements the WHATWG specification - possibly the > very same way as Lexbor, the currently used library - does. These > alternative implementations may have > different performance characteristics, platform requirements, or level > of maintenance/support, which may qualify them as more suitable for > some use-cases than what the built-in > ones can offer. If we make these classes final, there's no way to use > alternative implementations as a replacement for the default ones, > although they all implement the same > specification having mostly clear semantics. > > - Making the classes final, but adding a separate interface for each: > The impact of making the built-in classes final would be mitigated by > adding one interface > for each specification (I didn't like this idea in the past, but it now > looks much more useful in the perspective of the final vs non-final > debate). Because of the interfaces, > there would be a common denominator for the different possible > implementations. I'm sure that someone would suggest that the community > (aka PHP-FIG) > should come up with such an interface, but I think we shouldn't expect > someone else to do the work when *we* are in the position to do it the > best, as those interfaces > should be internal ones, since the built-in URI classes should also > implement them. > > If we had these interfaces, projects could use whatever abstraction > they want via composition, but they could more conveniently pass around > the same object everywhere. > > I intentionally don't try to draw a conclusion for now, first of all > because it already took me a lot of time to try to mostly objectively > compare the different possibilities, and > I hope that we can find more pros-cons (or fix my reasonings if I made > mistakes somewhere) in order to finally reach some kind of consensus.
Thought: make the class non-final, but all of the defined methods final, and any internal data properties private. That way we know that a child class cannot break any of the existing guarantees, but can still add convenience methods or static method constructors on top of the existing API, without the need for an interface and a very verbose composing class. --Larry Garfield