Semantic Versioning - why you should care

Published on – 4 minute read

While recording the videos for my upcoming PHP Package Development videos, I know that I wanted to cover semantic versioning and explain it in-depth. So rather than only having a video available, once the course is available in the next couple months, I also wanted to provide an extensive write-up on the topic.

Why you should care

When you develop a package - no matter what language it has - you will come to a point where you want to actually release it. So what do you do? You push your code to a system like GitHub, Bitbucket or maybe a private Gitlab server and want people to consume your package. This also has nothing to do with only public packages - but also with internal packages that will never be open sourced.

But just pushing your code is not enough. Sure, people can go and just clone your repository to get access to your files, but we've learned that we rather manage third-party code as dependencies of our own application and therefore use tools like NPM or Composer to make our lifes easier and manage these dependencies.

Of course, when using composer, you can also pull in packages directly from git branches. For example, to pull in a package from the master branch, this is what your composer.json would look like:

{
	"require": {
		"beyondcode/some-package": "dev-master"
	}
}

The dev- prefix already tells you that you are depending on a development version of your code. In this case, it's from the master branch.

But why is this bad?

As the name already says it, everyone that uses your package is going to depend on it. If your package has a bug, removes functionality or maybe even adds functionality in a specific way, the consumers of your package need to be sure that they can depend on your package without breaking their own application. You surely do not want to be responsible for breaking other peoples applications.

That's why you can and should version your packages.

Semantic Versioning

Semantic Versioning (or SemVer) tries to solve this problem by giving the version a semantic meaning. This means that you should not increase/decrease your package's version whenever you feel like it, but rather follow a set of simple rules.

Let's take a look at the structure of a version number:

1.10.2

Semantic Versioning describes the version as:

MAJOR.MINOR.PATCH

And the rules that apply to each of these version parts are simple:

  • Major version increases when you make incompatible API changes to your package.
  • Minor version increases when you add new functionality to your package that is still backwards compatible.
  • Patch version increases when you add bug-fixes to your package that are still backwards compatible.

In addition, everything that has a major version of 0 - like version 0.8.1 - is meant to be not stable and may change at any time and introduce breaking changes. Therefore you should try to avoid using dependencies of packages that do not have at least a major version of 1 in your application, since these packages might change their API at any time. If you need to use a package with a 0.x.x version, be sure to set the version constraints in your composer.json file as strict as possible. I'll talk more about version constraints and composer later.

On the other hand, if you are the maintainer of a package, you should try to release a 1.0.0 release to identify your package as stable.

When to tag 1.0.0

A common question when getting started with semantic versioning is "When should I tag a 1.0.0 release?". For all of my packages, I try to follow a very simple rule:

If you use the package in production, it should be a 1.0.0

Learning by examples

So this is pretty easy in theory, but let's take a look at a few examples.

Let's say that you have a package with a class that takes a Request class and allows us to retrieve query parameters by their name.

The code looks like this:

<?php

class QueryParameters
{

    public function __construct(RequestInterface $request)
    {
        $this->request = $request;
    }
    
    public function parseQueryParameters()
    {
    	$this->queryParameters = parse_str($this->request->getUri()->getQuery(), $queryParameters);
    }
    
    public function get($name)
    {
    	return $this->queryParameters[$name];
    }

}

Alright. Nothing fancy going on here. We get the request instance, parse the query parameters and the users of our package can make use of the get method to retrieve the query parameter values.

Let's take a look at some possible scenarios and how it would affect the versioning of our package.

To begin with, let's say our package uses version 1.0.0.

Bugfix

If you pay close attention to the get method, you can spot a bug in there. If the $name key does not exist on the array, we will get a PHP error.

Let's fix this:

<?php

class QueryParameters
{

    public function __construct(RequestInterface $request)
    {
        $this->request = $request;
    }
    
    public function parseQueryParameters()
    {
    	$this->queryParameters = parse_str($this->request->getUri()->getQuery(), $queryParameters);
    }
    
    public function get($name)
    {
    	return $this->queryParameters[$name] ?? '';
    }

}

Affected version change

How would this affect our version? We only fixed a bug in our code and everything is still backwards compatible and works as it did before. We can safely tag the new version 1.0.1.

New feature

Let's say we want to add a new method that also let's us return the raw query string. The code will look like this:

<?php

class QueryParameters
{

    public function __construct(RequestInterface $request)
    {
        $this->request = $request;
    }
    
    public function parseQueryParameters()
    {
    	$this->queryParameters = parse_str($this->request->getUri()->getQuery(), $queryParameters);
    }
    
    public function getRawQuery()
    {
    	return $this->request->getUri()->getQuery();
    }
    
    public function get($name)
    {
    	return $this->queryParameters[$name] ?? '';
    }

}

Affected version change

We have introduced a completely new method to our class. Those that are using version 1.0.0 or 1.0.1 can safely update to the new version since it does not affect the old code in any way. Since we've added a new feature we will tag this one as 1.1.0.

Now let's add another modification to our code. Right now users of our library need to call the parseQueryParameters method manually in order to access the parsed parameters. It would be a lot nicer if they could just access the data directly.

Let's call the parseQueryParameters method immediately after receiving the request in the constructor. In addition we no longer want it to be public and change the method visibility to private.

<?php

class QueryParameters
{

    public function __construct(RequestInterface $request)
    {
        $this->request = $request;
        
        $this->parseQueryParameters();
    }
    
    private function parseQueryParameters()
    {
    	$this->queryParameters = parse_str($this->request->getUri()->getQuery(), $queryParameters);
    }
    
    public function getRawQuery()
    {
    	return $this->request->getUri()->getQuery();
    }
    
    public function get($name)
    {
    	return $this->queryParameters[$name] ?? '';
    }

}

Affected version change

This is now going to introduce a breaking change, since users of version 1.0.x or 1.1.0 can no longer reuse the old versions. The parseQueryParameters method is no longer available to them. This results in tagging this code as version 2.0.0.

Using versions as a package consumer

Now that you know how semantic versioning works, let's take a look at the version ranges that you can define in your composer.json file. These ranges define what kind of updates you will receive when performing composer update.

Fixed/Pinned Version

If you only specify the version number in your composer.json file, you will only get this one specific version of the package. Nothing less and nothing more.

This is only useful if, for some reason, the maintainer of the package is not following semantic versioning, or if the maintainer has accidentaly introduced a bug in a bugfix release that you can not live with.

{
	"require": {
		"beyondcode/some-package": "1.0.0"
	}
}

Wildcards, comparators and logical operators

You can also use wildcards (*) and comparators (<, <=, > and >=) in your composer.json file to specify the versions that you want to use. In addition to these you can use logical operators to combine them. The comma (,) is used as an AND operator while two pipes (||) act as an OR operator. This way you can combine multiple rules to get the desired version constraints.

For example, to allow all bugfix releases between 1.1.0 and 1.2.0 you could use the following:

{
	"require": {
		"beyondcode/some-package": ">=1.1.0,<1.3.0"
	}
}

But this could also be noted like this:

{
	"require": {
		"beyondcode/some-package": "1.1.* || 1.2.*"
	}
}

~ Tilde operator

There are also two special operators to use in your composer.json file. The first one is the tilde (~) operator. This one is best explained by using some examples:

  • ~1.1 is equivalent to >=1.1,<2.0.0
  • ~1.1.0 is equivalent to >=1.1.0,<1.2.0

It is most commonly used to mark a minimum version, while the last specified version digit is allowed to go up.

^ Caret operator

This is probably the most used version constraint operator. It behaves similar to the tilde operator but is closer to semantic versioning.

For example:

  • ^1.1.3 is equivalent to >=1.1.3,<2.0.0
  • ^1.1 is equivalent to >=1.1.0,<2.0.0 - so the same as ~1.1

Basically, the caret operator tries to stick very close to semantic versioning and therefor allow more possible versions. Because as we have seen, if the maintainer (maybe you) pay attention to the versioning schema they use, it's safe to update in between major versions of packages.

Learning more about PHP package development

As I mentioned, I have started working on a new video course called PHP Package Development that is set to be released early this year and show you how to create your own reusable PHP packages for yourself, your company or for the whole world on GitHub.

If you are interested in learning more about PHP package design, be sure to sign up and get notified when the course launches, as well as receive a launch discount code.