Interconnected spider threads with water drops
Profile picture for user jakub
Written by
Jakub Piasecki
Published on
March 17, 2021

How to make sure your project will keep working in a world where "the drop is always moving"

As you may already know, Drupal is an extremely powerful framework with a high level of customizability, and you can basically bend Drupal to your will using the User Interface (UI). For example, you can:

  • add listing pages using Views UI
  • build complex layouts with Layout Builder
  • add custom fields to existing entities
  • adjust Drupal Commerce checkout flows directly from UI

However, if you know how to code, there's a whole other world out there, just hiding beneath Drupal´s surface: the world of APIs.

What is an API?

According to Wikipedia (“API”, 2021): 

an application programming interface (API), is a computing interface that defines interactions between multiple software intermediaries. It defines the kinds of calls or requests that can be made, how to make them, the data formats that should be used, the conventions to follow, etc. It can also provide extension mechanisms so that users can extend existing functionality in various ways and to varying degrees.

Drupal consists of several APIs, e.g. Form API, Plugin API, or Render API, to name a few (you can find a comprehensive list and detailed instructions under this link). Drupal Core, modules distributed as part of Drupal Core, contrib modules and custom modules all use Drupal APIs to seamlessly interact with each other based on well-established principles. Drupal, similarly to other PHP frameworks, utilizes the PHP dependency manager Composer in order to benefit from the many standalone PHP packages, where each of them provide additional functionality to Drupal, usually via an API.

“The drop is always moving”

The Drupal project is committed to provide a stable framework, while also innovating. You get new modules and APIs with almost every minor release of Drupal. The same goes for the contrib module ecosystem; you can come across new versions of contrib modules every day, and quite often they feature new ways of interacting with them. As an example of this, check out the recent version of Drupal Commerce 2.24 release notes, which features a new Order Preprocess API.

Semantic Versioning

At this time, I have to mention Semantic Versioning, which was developed in order to establish a versioning scheme for packages that depend on each other. According to semver.org (2021), Semantic Versioning can be summarized like this: 

Given a version number MAJOR.MINOR.PATCH, increment the:

  • MAJOR version when you make incompatible API changes,
  • MINOR version when you add functionality in a backward-compatible manner, and
  • PATCH version when you make backward-compatible bug fixes.

At the time of writing this blog post, the most recent release of Drupal Core is 9.1.4. Let's break down this version into segments, starting from the least significant bit:

Patch version 4 means there were four extra releases with bug fixes since the minor version release. Minor version 1 (initially released as 9.1.0), is the successor of the 9.0.x line. It features new functionality (amongst other the Olivero frontend theme), but is delivered to you in a backward-compatible way (which, I don’t have to say, is awesome!). The difference between 9.x.x and 8.x.x, also known as major releases, is that 9.x.x provides changes that are backward incompatible. This means that some of the APIs that were available in Drupal 8, no longer are available in Drupal 9.

Drupal contrib modules are also subject to semantic versioning. However, for historical reasons some of the modules are still featuring legacy versioning schemes, based on which major version of Drupal Core they are compatible with. For example, the latest Drupal Commerce release is 8.x-2.24. This means that the module is compatible with Drupal Core 8, the major release is 2, and the minor release is 24. Fortunately, modules can now be compatible with more than one core version, so the first part of the legacy version scheme is not applicable anymore. That is why the Drupal Infrastructure team came up with a method of translating old version strings into semver-compatible ones, which is also required by Composer. In practice, this means that version 8.x-2.24 is mapped to 2.24.0.

Trust your muscles 💪, not your memory🤔

Let’s consider a basic scenario. I have already mentioned the new Order preprocess API featured in Drupal Commerce 2.24(.0). Let’s say you’ve been using your project for a while now, and this is what the project’s root composer.json file looks like:

{
    "name": "drupal/recommended-project",
    "description": "Project template for Drupal 8 projects with a relocated document root",
    "type": "project",
    "license": "GPL-2.0-or-later",
    "repositories": [
        {
            "type": "composer",
            "url": "https://packages.drupal.org/8"
        }
    ],
    "require": {
        "composer/installers": "^1.2",
        "drupal/core-composer-scaffold": "^8.8",
        "drupal/core-project-message": "^8.8",
        "drupal/core-recommended": "^8.8",
        "drupal/commerce": "^2.0" <------------------- note this one here
    },
    "require-dev": {
        "drupal/core-dev": "^8.8"
    },
    "conflict": {
        "drupal/drupal": "*"
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "config": {
        "sort-packages": true
    },
    "extra": {
        "drupal-scaffold": {
            "locations": {
                "web-root": "web/"
            }
        },
        "installer-paths": {
            "web/core": ["type:drupal-core"],
            "web/libraries/{$name}": ["type:drupal-library"],
            "web/modules/contrib/{$name}": ["type:drupal-module"],
            "web/profiles/contrib/{$name}": ["type:drupal-profile"],
            "web/themes/contrib/{$name}": ["type:drupal-theme"],
            "drush/Commands/contrib/{$name}": ["type:drupal-drush"],
            "web/modules/custom/{$name}": ["type:drupal-custom-module"],
            "web/themes/custom/{$name}": ["type:drupal-custom-theme"]
        }
    }
}

Now, I want to focus on the Drupal Commerce version constraint ^2.0. This means all versions of Drupal Commerce starting with 2.0.0 are fine, and updates are allowed (2.0.1, 2.1.0, 2.8.99, etc.) until a new major release is tagged (3.0.0). This version constraint allows you to update your dependency in a backward-compatible way. Perfect!

Now, let’s say you decided to utilize the Order Preprocess API as part of the project. As you can code and you know how to implement such an API, you start by updating your dependency with composer update drupal/commerce --with-dependencies (or composer update), in order to download the latest version of Drupal Commerce. Next, you implement a new tagged service, and some might say the work is done. But, then you would be forgetting one important thing: version constraints allow composer dependencies to be upgraded as well as downgraded. Even though it doesn’t happen that often with modules, you may have noticed that some dependencies (such as Symfony components), are downgraded from time to time when new dependencies are added to a project.

This is where you can make use of your muscles: while utilizing the new API that was introduced in Commerce 2.24, you should also bump the version constraint to match the minimum version of Commerce that is allowed for your code to work. This means you should change the mentioned version constraint from initial

          “drupal/commerce”: “^2.0”

to

          “drupal/commerce”: “^2.24”

This way you make sure Composer will never resolve a version that is incompatible with your custom code.

Chained dependencies

There’s one more thing people tend to forget about: chained dependencies. To exemplify a chained dependency, let's continue using Drupal Commerce as a dependency that is referenced from your project’s composer.json. As you may already know, Commerce comes with a bunch of dependencies on its own. However, you should not take Commerce dependencies for granted. Commerce may add new dependencies, bump minimal versions of existing dependencies, or even remove some of them! Therefore, you should always explicitly specify the versions of chain dependencies required to run your project.

As an example, let’s say you decided that you have to implement the entity.duplicate event provided by the Entity module, which was already conveniently bundled with Drupal Commerce. You then added all the code required to implement that API (in this case an event subscriber), in a custom module committed directly to your project. However, to make sure the dependency will be there even if Commerce decides to remove their dependency on the Entity module, you must add drupal/entity:^1.0 directly to your project’s composer.json. This can be done by running the following composer command:

composer require drupal/entity:^1.0

This will make sure that the entity module ain’t going anywhere, but also that it’s not all of a sudden bumped up to major version 2. This would remove the API you’ve implemented, given that such a removal can only be done in a major release.

Hi, have you met Stan?

Remember, you’re not alone. The best way to mitigate the risk of losing the dependency or getting it resolved without the API your project relies on, is to have decent test coverage. Even a simple unit test that is run on every pull (or merge) request can save you a lot of trouble. However, even with the best intentions, some features may not be covered by your tests. In that case, remember you can always come back and iterate on missing test coverage.

Stan, or actually phpstan, is yet another tool that can be helpful (big kudos to Matt Glaman for adding Drupal support to phpstan!). Stan ensures a certain level of quality by performing static analysis of your codebase searching for incorrectly implemented APIs, missing code references, etc. Some of the biggest advantages of using static analysis tools, is that running them takes little time, and that they have no dependency on the database. This means that setting up your CI platform should be relatively simple (more about this in a future blog post, so stay tuned!). Nevertheless, such tools should be used with caution and limited trust as they are not able to detect all issues with your code. One example is hook-based API, which the lack of cannot be easily tested with static analysis tools, as it relies on the naming pattern for globally accessible functions.

Summary

Hopefully, after reading this blog post you’re aware of what can go wrong if you carelessly implement a new API in a project that must be maintained over a longer period of time. Make it a habit to always double-check dependencies you’re directly relying on. This means that you will be able to perform updates to all dependencies when they come out, without risking that they might ruin your application. So, the best way to keep the future you happy, is to remember two simple rules:

  • bump existing dependency constraints every time you implement a new API addition
  • add chained dependency as a direct project dependency whenever you start directly relying on the API it provides

The future you will thank you.

References

“API”. 2021, In Wikipedia. https://en.wikipedia.org/wiki/API
Semver.org. (2021). Semantic Versioning 2.0.0. https://semver.org/