Semantic Versioning and Public Interfaces

Adherence to Semantic Versioning is just The Right Thing To Do, but it turns out you have to be extra-careful when modifying public interfaces to maintain backwards compatibility. This is obvious on reflection, but I never thought about it beforehand. Thanks to Hari KT for pointing it out.

Why do you have to be extra-careful with interfaces and SemVer? To see it more clearly, let’s use a concrete class as an example.

class Foo
{
    public function bar() { ... }
    public function baz() { ... }
}

If we remove a public method, that’s clearly a BC break. If we add a non-optional parameter to an existing public method, that’s also clearly a BC break. Those kinds of changes will break any code already using the Foo class; that code will have to be modified to accommodate the changes in Foo.

However, if we add a new public method to the concrete class, that is not a BC break. Likewise, changing the signature of an existing method to add an optional parameter is not a BC break either. (UPDATE: See note below.) Code using the Foo class does not have to change to accommodate the new method or the new optional parameter.

But what happens with an interface?

interface FooInterface
{
    public function bar();
    public function baz();
}

Removing a method, or adding a non-optional parameter to an existing method, is the same as with a concrete class: it’s BC break.

However, unlike with a concrete class, adding methods is also a BC break. So is changing an existing method sigature to add an optional parameter. Existing code that implements FooInterface will no longer comply with the interface; that code will have to change to accommodate the new method or the new optional parameter.

Thus, interfaces are more susceptible to BC breaks than concrete classes. Once an interface is published as “stable”, I don’t see how it can be changed at all per the rules of SemVer (that is, unless you bump the major version). The only thing you can do to maintain BC is add an entirely new interface to the package while leaving the old one in place, perhaps even extending the old one if needed.

Again, it’s obvious in hindsight, but I did not have the foresight to anticipate it. Perhaps this realization will save you some trouble in the future.

p.s. On further examination, the interface constraints apply to abstract classes, too. Changes to an abstract mean that child classes will have to be modified as well, otherwise they won’t comply with the modified abstract.

UPDATE: Dave Marshall notes on Twitter that in the concrete class case, if you add an optional parameter to a method, and a user has extended and overridden that method, the extended class will break under PHP E_STRICT. He links to Symfony policy on the subject, which I will now have to read.


Are you stuck with a legacy PHP application? You should buy my book because it gives you a step-by-step guide to improving your codebase, all while keeping it running the whole time.

11 thoughts on “Semantic Versioning and Public Interfaces

  1. The only thing you can do to maintain BC is add an entirely new interface to the package while leaving the old one in place, perhaps even extending the old one if needed.

    This is exactly what we’ve done in Zend Framework a number of times. The default concrete implementations then implement both the original and any new interfaces. When we bump to a new major version, we can combine them once again.

    Painful to accommodate and implement, but far less painful on your users!

    • How did you go about naming the new interfaces?

      Vendor\Thing\UsefulInterface
      Vendor\Thing\V2\UsefulInterface

      Or was the updated interface different enough that it could have its own name?

      • As you are talking about v1 and v2 , you can name it anyway 😉 . Everyone choose the first option mostly. Some may change though. ( Eg : Guzzle v5 etc )

  2. But what happens with an interface? Removing a method […] is the same as with a concrete class: it’s BC break.

    I wonder why do you think so? Maybe formally it is, but actually it’s not. If classes had already once implemented interface, they all have required methods, and all works fine.

    On the other hand, it’s broken for the further development: the next class that will implement that interface will not implement an absent method and will not work in some scenarios. But still, old code is OK, and for the new code you can do some workaround (like: get error → notice absent method → add it). I think it’s not obviuos, and you should add a separate section with explanations.

    ps: …changing an existing method signature…

    • That one’s a little more involved, and while you address it, I think it’s still a BC break, at least in terms of expectations. Stick with me here.

      1. FooInterface 1.0 is published with bar() and baz().

      2. Implementation X is published against FooInterface.

      3. A user or project typehints against FooInterface, uses Implementation X, and calls X::bar().

      4. FooInterface 1.1 is re-published without bar().

      5. Implementation Y is published against the new FooInterface, and lacks bar().

      6. The user or project from #3 adds, or switches to, Implementation Y. Code breaks because Y::bar() is not available.

      Maybe that’s a bit tricky, but I think it illustrates the case.

  3. Just my opinion, but i think you have decide between interface and implementation on a package level. You have either a standalone package which contains the interfaces + implementation or just a package with interfaces (like PSR-X). No one should ever rely on partial code (like picking the interfaces only).

    But of course, this doesn’t solve the other issues with SemVer and interfaces. The best approach seems to be versioned namespaces, as they are also solving the issue of dependency versions. E.g. class Foo implements BarInterface {} But which version of BarInterface? Not everything is managed via Composer.

Leave a Reply

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