composer.json - Understanding version constraints and dependencies

If you have been working with Contao for a while, you will sooner or later come into contact with the composer.json file. In this blog post, we'll take a closer look at it, explore the topic of version constraints and I'll give you tips on how to deal with error messages if certain versions cannot be installed.

The following article does not claim to be complete or to cover all possibilities. The topic is too extensive for that. The aim is rather to give you an overview and a better understanding. You can find out more about Composer in the official documentation.

Contao, packages and the world of Composer

In the days of Contao 3, you could simply download Contao as a complete ZIP package and transfer it to your server. This changed fundamentally with the release of Contao 4.0 in 2015. Since then, Contao has been based on the Symfony framework and requires Composer for package management. Contao 5 consists of almost 190 individual packages. Wow, that's a lot!

So that Composer knows which packages are required, there is a configuration file, the composer.json. Among other things, this file defines which Contao version, extensions and dependencies are required for the installation.

As a result, almost every Contao installation is a unique combination of packages and versions. Even if two installations have the same Contao version installed, maybe not all packages are identical. This is because Composer always tries to find the best possible package combination at the time of execution. Your platform is also included in this best possible package combination. In other words, the PHP version used, PHP extensions, etc.

The currently installed status with exact versions of the packages including their dependencies can be found in the composer.lock file.

The composer.lock is automatically created by Composer and must never be changed manually.

To make the whole thing a little more tangible, here is a comparison to baking a cake.

The recipe (composer.json) contains the various ingredients (packages) and the permitted quantities (version constraints) for baking an apple pie (Contao 5). For example, the quantity of apples is specified as 5 - 6 large apples (version constraint). Before we start baking, we gather all the ingredients (composer update) and prepare them. For example, we now have exactly 1042 grams of sliced apples in our bowl. The exact amount of all ingredients (composer.lock) is now ready for the next step. Now we mix the ingredients together and bake (composer install) our cake (Contao 5).

If my mother and I bake the same cake (Contao 5.2.7), the result will most likely be different. This is simply due to the fact that I have a different oven (platform). In addition, after cutting the apples, I only have 1012 grams instead of 1024 grams. In the end, we have both baked a delicious apple pie, but both will taste slightly different.

And that's exactly how it is with Contao and Composer. In the end, we both have Contao 5.2.7 installed after the installation, but most likely not the exact same versions of all packages will be installed.

Structure of the composer.json file

Now, let's look at the structure of the composer.json. Let's take the composer.json of Contao 5.x as an example.

You can find the file in the root directory of Contao. It contains various information such as the name of the project, a description, the authors, the license type and a list of the required packages. There are also further entries such as conflict, extra, scripts and more, which we will not go into here. You can find more details about the individual parameters of the composer.json schema on the Composer website.

We mainly want to deal with the packages to be installed.

Definition of packages to be installed

The "require" section is used for this purpose. Here you will find a list of packages that are used for our installation. The packages are specified as key-value pairs, where the name of the package is the key and the version or version constraint is specified as a value. For Contao 5.3, for example, the require part looks like this:

"require": {
    "contao/calendar-bundle": "^5.3",
    "contao/comments-bundle": "^5.3",
    "contao/conflicts": "@dev",
    "contao/faq-bundle": "^5.3",
    "contao/listing-bundle": "^5.3",
    "contao/manager-bundle": "5.3.*",
    "contao/news-bundle": "^5.3",
    "contao/newsletter-bundle": "^5.3"
},

As we do not currently use any extensions, you will only see the Contao bundles, the manager bundle and the contao/conflicts in the list. Yanick has already explained the function of contao/conflicts in detail in a previous blog post.

In our example, Contao 5.3 should now be installed, as well as the corresponding bundles for calendar, comments, FAQ, listing, news and newsletter. If you take a closer look at the version number, there is an additional character in addition to the version number. For example, the calendar bundle has the caret symbol ^ and the version of the manager bundle is followed by a wildcard *. This brings us to the topic of version constraints.

Version constraints in the composer.json

These constraints determine which versions of a package can and cannot be installed. Composer always tries to install the latest packages. There are various ways to define the versions. You can find an overview of all options in the Composer documentation.

Most often you will see one of the following constraints:

  • Exactly: 1.2.5
    Means that only the exact version 1.2.5 may be installed.
  • Greater than or equal to: >=1.2
    Means everything that is higher than or equal to version 1.2 - but be careful, versions 2.0 or 3.0 are also higher than 1.2!
  • Wildcard: 1.2.*
    Means that the position with the asterisk may change. So all 1.2.x versions, but not 1.3.0
  • Caret Version Range ^1.3
    Means all versions greater than 1.3, but not version 2.0. This means that all non-breaking versions up to the next major version are permitted.
  • Logical OR: ^4.13 || ^5.0
    Means all versions greater than 4.13 but not version 5.0 OR all versions greater than 5.0 but not version 6.0

With this knowledge, let's take another look at the version conditions in the composer.json of Contao 5.3:

"contao/calendar-bundle": "^5.3", // all >=5.3.0, but <6.0.0

"contao/manager-bundle": "5.3.*", // all versions 5.3.x but not 5.4

But that's not all so far. Because each of these packages now has its own composer.json.

The composer.json of the manager-bundle looks like this:

"require": {
    "php": "^8.1",
    "ext-json": "*",
    "contao/core-bundle": "self.version",
    "contao/manager-plugin": "^2.4",
    "doctrine/dbal": "^3.6",
    "doctrine/doctrine-bundle": "^2.8",
    "friendsofsymfony/http-cache": "^2.6",
    "friendsofsymfony/http-cache-bundle": "^2.6",
    "nelmio/cors-bundle": "^2.0.1",
    "nelmio/security-bundle": "^2.2",
    "symfony/cache": "^6.4",
    "symfony/config": "^6.4",
    "symfony/console": "^6.4",
    "symfony/debug-bundle": "^6.4",
    "symfony/dependency-injection": "^6.4",
    "symfony/deprecation-contracts": "^3.0",
    "symfony/doctrine-bridge": "^6.4",
    "symfony/dotenv": "^6.4",
    "symfony/error-handler": "^6.4",
    "symfony/expression-language": "^6.4",
    "symfony/filesystem": "^6.4",
    "symfony/finder": "^6.4",
    "symfony/framework-bundle": "^6.4",
    "symfony/http-client": "^6.4",
    "symfony/http-foundation": "^6.4",
    "symfony/http-kernel": "^6.4",
    "symfony/mailer": "^6.4",
    "symfony/monolog-bridge": "^6.4",
    "symfony/monolog-bundle": "^3.1",
    "symfony/process": "^6.4",
    "symfony/routing": "^6.4",
    "symfony/security-bundle": "^6.4",
    "symfony/stopwatch": "^6.4",
    "symfony/twig-bundle": "^6.4",
    "symfony/web-profiler-bundle": "^6.4",
    "symfony/yaml": "^6.4",
    "toflar/psr6-symfony-http-cache-store": "^4.0",
    "twig/extra-bundle": "^3.0"
},

As you can see here, numerous other dependencies (PHP version, Contao core, Symfony, Twig, etc.) are defined that we did not see at first glance in our Contao root composer.json.

If we now go further, the symfony/framework-bundle package also has a composer.json. Here the required part looks like this:

"require": {
    "php": ">=8.1",
    "composer-runtime-api": ">=2.1",
    "ext-xml": "*",
    "symfony/cache": "^5.4|^6.0|^7.0",
    "symfony/config": "^6.1|^7.0",
    "symfony/dependency-injection": "^6.4|^7.0",
    "symfony/deprecation-contracts": "^2.5|^3",
    "symfony/error-handler": "^6.1|^7.0",
    "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
    "symfony/http-foundation": "^6.4|^7.0",
    "symfony/http-kernel": "^6.4",
    "symfony/polyfill-mbstring": "~1.0",
    "symfony/filesystem": "^5.4|^6.0|^7.0",
    "symfony/finder": "^5.4|^6.0|^7.0",
    "symfony/routing": "^6.4|^7.0"
},

By now you will understand why resolving package dependencies is very complex. In addition to the dependencies found, the PHP version present on the server and other server properties must also be taken into account. Corresponding conflict entries must also be respected. Fortunately, we have Composer, which takes care of the resolution for us.

However, this also means that we may want to install or update a certain extension, but this is not possible due to other dependencies, of which there are many.

Helpful commands and tips for trouble

To make your work and troubleshooting easier, I will give you a few tips in the following paragraph on how you can better understand error messages. I will also show you three commands for Composer that can help you in this context.

Example 1 : Finding out dependencies

Let's assume that you have noticed that the Notification Center extension (terminal42/notification_center) is installed, although you don't see it in the list of packages in the Contao Manager. There is also no entry in your root composer.json.

There are several ways to find out why the Notification Center is installed.

For example, you can start the Contao Manager, search for the Notification Center package and then click on the "Dependents" tab.

You can also execute the following command via the console.

php composer.phar depends terminal42/notification_center

The result could look like this:

isotope/isotope-core 2.8.17 requires terminal42/notification_center (^1.0)
terminal42/notification_center 1.6.14 replaces contao-legacy/notification_center (self.version)

You can see that the extension isotope/isotope-core has defined the package terminal42/notification_center as a dependency and was therefore installed.

If you want to see the entire tree of dependencies, use the -t parameter.

It will look like this:

php composer.phar depends -t terminal42/notification_center

Of course, the list can also be much more extensive. Let's assume we want to find out all the dependencies of twig/twig. Then the result looks like this:

php composer.phar depends twig/twig

If you display this in the tree view, you will see a result that is more than 1260 lines long.

Example 2 : An extension cannot be installed under Contao 5

At the time of writing this blog post, the Notification Center is not yet compatible with Contao 5. If you start an installation via the Contao Manager, you will receive an error message like this:

> Resolving dependencies using Composer Cloud v3.6.0-1-g9d9036c
[6.8MiB/0.14s] Loading composer repositories with package information
[71.9MiB/11.66s] Updating dependencies
[100.6MiB/12.09s] Your requirements could not be resolved to an installable set of packages.
[100.6MiB/12.09s]
  Problem 1
    - terminal42/notification_center[1.7.0, ..., 1.7.3] require contao/core-bundle ^4.13 -> found contao/core-bundle[4.13.0, ..., 4.13.35] but these were not loaded, likely because it conflicts with another require.
    - Root composer.json requires terminal42/notification_center ^1.7 -> satisfiable by terminal42/notification_center[1.7.0, 1.7.1, 1.7.2, 1.7.3].
[100.6MiB/12.09s] <warning>Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.</warning>
[39.8MiB/12.13s] Memory usage: 39.76MB (peak: 202.63MB), time: 12.14s.
[39.8MiB/12.13s] Finished Composer Cloud resolving.

The reason why the notification_center is not installed can be found in this line:

terminal42/notification_center[1.7.0, ..., 1.7.3] require contao/core-bundle ^4.13 -> found contao/core-bundle[4.13.0, ..., 4.13.35] but these were not loaded, likely because it conflicts with another require.

Composer tells you that the package notification_center requires Contao ^4.13, but cannot install it because there is a conflict with another required package.

In our case, we have specified in our root composer.json that we want Contao ^5.2 and no Contao 4.13 is allowed.

If you have a problem with the installation of an extension, it is very helpful if you look at the dependencies. You can do this in the Manager via the "Dependencies" tab, at Packagist or directly in the composer.json of the extension.

Here you can see that Contao Core with the version condition ^4.13 is required and therefore no Contao 5 is allowed.

The popup in the Contao Manager only shows the information for the latest version. You can see this in more detail at packagist.org, where you can select this for each version.

Example 3 : The latest version of an extension is not installed

Let's assume you are currently using Contao 4.9 and PHP 7.4 and have installed the contao/page_image extension in version 4.1.

You have now seen in the Contao Manager that version 4.2.1 is already available. So you update all packages, but version 4.2.1 is not installed. Version 4.1 remains installed.

You can now use the "Dependencies" in composer.json to find out where the problem lies or you can use the command:

php composer.phar prohibits terminal42/contao-pageimage 4.2.1

The answer looks like this:

terminal42/contao-pageimage 4.2.1      requires         php (^8.1 but 7.4.33 is installed)           
terminal42/contao-pageimage 4.2.1      requires         contao/core-bundle (^4.13 || ^5.0)           
contao/managed-edition      dev-master does not require contao/core-bundle (but 4.9.42 is installed)

In this case, the cause is actually two dependencies. PHP ^8.1 is required, but 7.4 is installed. In addition, Contao ^4.13 or ^5.0 is required, but we are using Contao 4.9.

You can only install the latest version of the extension if both dependencies are fulfilled.

Example 4 : Find all installations with an outdated extension

The previous examples all refer to a single installation. But what if you want to know which installations do not yet have contao-pageimage 4.2.1 installed? In this case, trakked can help you.

Using the package filter and a version constraint, you can search for it specifically. All you need to do is restrict the extension terminal42/contao-pageimage in the package filter and also enter the version condition <4.2. You will now receive a list of all affected installations.

Conclusion

I could certainly list many more examples, but that would go beyond the scope here.

I hope this blog post has helped you to better understand the topic of versioning and motivated you to delve a little deeper into the background of composer.json and further expand your knowledge.

Add a comment

Please add 8 and 9.