Skip to Content
Course content

170: Semantic Versioning for PHP Packages

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine this: You've got a stable production app. You're doing some routine maintenance, you run composer update to grab the latest bug fixes for your dependencies, and suddenly, your payment processing pipeline starts throwing fatal errors. You haven't touched a single line of your own code in weeks.

Fatal error: Uncaught TypeError: Argument 1 passed to \PaymentGateway\Client::charge() must be an instance of \PaymentGateway\PaymentRequest, string given in /var/www/html/src/Checkout.php:42

You check your composer.json and see this:

"require": {
    "payment-gateway/sdk": "^1.2.0"
}

You're thinking, "Wait, I used the caret symbol (^). That's supposed to be safe. It only allows non-breaking updates." You dig into the package's changelog and find that version 1.3.0 was released yesterday. The developer changed the charge() method to require a PaymentRequest object instead of a simple string. They called it a "feature improvement" and bumped the minor version.

The Betrayal of the Caret

Here is the problem: the package maintainer violated the rules of Semantic Versioning (SemVer). By releasing a breaking change in a minor version (1.2.0 → 1.3.0), they broke the implicit contract they had with you. When you tell Composer ^1.2.0, you are saying: "I trust this maintainer to only give me updates that don't break my existing code."

In a world that follows SemVer, that charge() method change should have triggered a Major version bump. The release should have been 2.0.0. If it had been, Composer would have seen that 2.0.0 is outside the range of ^1.2.0 and would have left you safely on the latest 1.x release.

The Anatomy of a Version Number

If you're writing your own packages—or even just managing internal libraries for your team—you need to be disciplined about these three numbers: MAJOR.MINOR.PATCH.

  • MAJOR: You've changed the API in a way that is not backward-compatible. If a developer updates to this version, their code will likely break. You've renamed methods, changed argument types, or removed classes.
  • MINOR: You've added functionality in a backward-compatible manner. You added a new method to a class or an optional parameter to a function. Existing code keeps working, but new features are available.
  • PATCH: You've fixed bugs. No new features, no breaking changes. Just making the existing logic actually work as intended.

I've seen a lot of developers hesitate to bump the Major version because "2.0.0 feels like a huge deal." Trust me, it's not. A Major bump is a signal of respect for the consumer's time. It tells them, "Hey, don't just run composer update in production; you'll need to actually read the migration guide for this one."

Regaining Control with Constraints

So, how do you fix the broken production site? You have two choices. The "correct" way is to wait for the maintainer to fix their versioning, but you can't let your site stay down.

You can "pin" the version to prevent the breaking update from sneaking back in. Change your composer.json to:

"require": {
    "payment-gateway/sdk": "~1.2.0"
}

The tilde (~) is more restrictive than the caret. ~1.2.0 tells Composer: "I want the latest patch versions of 1.2, but do not move to 1.3." This buys you time to rewrite your Checkout.php to use the new PaymentRequest object without the fear of another surprise update breaking things again.

My advice? Always assume someone, somewhere, will mess up their SemVer. Use composer.lock in your version control, and never run composer update on a production server. Test the updates in a staging environment first so that a "minor" update doesn't become a major headache.




📋 Practical Task

Correcting the Version Bumps for "AuthLib"

You are the lead maintainer for a PHP authentication library called AuthLib. You have a list of pending changes that need to be released. Based on the principles of Semantic Versioning, determine whether each change requires a MAJOR, MINOR, or PATCH bump.

Current Version: 3.4.2

Analyze the following changes and determine the new version number for each:

  • Change A: You found a bug where hashPassword() occasionally returned a null value when the salt was empty. You fixed the logic to ensure a string is always returned.
  • Change B: You added a new method verifyTwoFactorToken() that allows users to implement 2FA. This does not affect any existing methods.
  • Change C: To improve security, you changed the login() method. It previously accepted login($username, $password), but it now requires a UserCredentials object as the first argument.

Your Task: Write down the resulting version number for each change, assuming they are released one after another in the order A, B, then C.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.