Composer as a package manager in Magento 2

In the past, there was a big challenge to keep a package-based PHP project up to date. We had sort of package managers like PEAR and PECL, but frequently they caused more challenges rather than provided a handy possibility to install and manage packages quickly. Fortunately, the dark age of the mentioned managers is history now, as the Composer comes to fix difficulties. With every new version, Composer has seen significant improvements, and many modern platforms/frameworks like Symfony, Laravel and Magento use Composer as a part of their systems.

Composer allows you to declare a list of required packages, their dependencies and to automate the package installation or to make an upgrade process easier. But, honestly, the package manager configuration and functionality might not be easy to understand especially in the beginning.

Composer has two types of configuration files: composer.json and composer.lock. The JSON configuration file is included in the main project as well as in every particular package. In a nutshell, Composer needs to know which packages to install and from which sources. This information is declared in the main project’s JSON configuration files. At the same time, every package has its own requirements (dependencies, platform versions). The package’s JSON configuration file contains information about the mentioned requirements. The composer.lock configuration file is actual only for the main project. Particular packages don’t contain this configuration file.

Then, why do we need two different configuration files and what is the purpose of the composer.lock file? Let’s imagine that we’ve created a package-based project. We have ensured that our project works well with a package foo_v1.5.1 and a package bar_v.2.2.6. But we also want to have a possibility to upgrade these packages for our project in the future once a new package version is released. So, in our composer.json file we can declare the required package version as foo_v1.5.* (which means >=1.5 and < 1.6). However, we are not sure yet that our project will work with foo_v1.5.2 since it’s not released yet and we had no chance to test our project with the new package version. That’s why we cannot allow Composer to install the latest version of the package upon our project installation. Here the composer.lock file comes as a life saver. At this point, the “.lock” extension becomes self-explanatory. With the .lock file, we can literally lock the exact package version of our project. Fortunately, we don’t have to create the lock file manually. It will be created automatically by Composer after running the composer update command.

The next roadblock for the Composer “newcomers” is the difference between composer install and composer update commands. First of all, the install command is available only if we have composer.lock file present in the project. Basically, the package manager ignores all records from composer.json configuration upon the installation command. The installation command will collect all information from the lock file and install the exact package versions that are declared in this file. But how does Composer know about the exact version if we declared the version suffixed with the asterisk – foo_v1.5.*? If the version is declared using a wildcard or a version range, Composer will create the corresponding record in the lock file that equals the last version, available at the moment, that matches the rule. So, if the latest build of the foo_v1.5 package is 1.5.6, Composer will create a record in the composer.lock file exactly for this version. And that is the point where composer update command takes place.

The update command generates the composer.lock file with exact versions of packages, as it was mentioned previously, and runs the composer install command to install all packages. Now let’s talk about the pitfalls. When you pull a Composer-based project from a repository you can surely run the composer install command. In most cases, if your system meets all the requirements, you will have the project successfully installed. Still, you should be careful with the composer update command. As you can guess, this command regenerates the lock file and you might have the packages version installed that has never been tested with the current version of the project. If you are lucky enough and the project developer uses version ranges wisely, probably you will still have the working project, perhaps with some minor bugs. However, more often than not, things can get worse.

One of the great Composer features is its own autoloader. By using the package manager, we can avoid countless require('here/is/a/long/path/to/the/file') declarations or SPL autoload registration. Composer usually uses PSR-4 autoloading and also supports PSR-0 autoloading. These scary abbreviations mean nothing else but a set of standards for the namespaces declaration in a scope of a PHP project.

Now let’s dig deeper into the composer.json file structure in a scope of Magento 2 application. By opening the mentioned file in a text editor you will see the following configuration:


{
    "name": "magento/magento2ce",
    "description": "Magento 2 (Community Edition)",
    "type": "project",
    "version": "2.2.0-dev",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "require": {
        "zendframework/zend-stdlib": "~2.4.6",
        "zendframework/zend-code": "~2.4.6",
        …
    },
    "require-dev": {
        "phpunit/phpunit": "4.1.0",
        "squizlabs/php_codesniffer": "1.5.3",
        "phpmd/phpmd": "@stable",
        … 
    },
    "replace": {
        "magento/module-marketplace": "100.2.0-dev",
        "magento/module-admin-notification": "100.2.0-dev",
        "magento/module-advanced-pricing-import-export": "100.2.0-dev",
        "magento/module-analytics": "100.2.0-dev",
        "magento/module-authorization": "100.2.0-dev",
        …
    },
    "extra": {
        "component_paths": {
            "trentrichardson/jquery-timepicker-addon": "lib/web/jquery/jquery-ui-timepicker-addon.js",
            …
        }
    },
    "autoload": {
        "psr-4": {
            "Magento\\Framework\\": "lib/internal/Magento/Framework/",
            "Magento\\Setup\\": "setup/src/Magento/Setup/",
            "Magento\\": "app/code/Magento/"
        },        
        "files": [
            "app/etc/NonComposerComponentRegistration.php"
        ]
    },
    "autoload-dev": {
        "psr-4": {
            "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/",
            …            
        }
    },
    "minimum-stability": "alpha",
    "prefer-stable": true
}

The initial lines provide meta information. It’s used by Composer repositories for getting general information about packages and projects. In the require section there is a list of required packages for the current project with the versions specified. As we have mentioned before, the package version might be represented either as an exact version number or by using different patterns for choosing a possible version number range. More information about the patterns can be found in the official Composer documentation. The require-dev section is quite similar to the require section. It contains a list of required packages but for a development environment. All packages listed in this section will be installed by default. We can disable the development packages installation by adding --no-dev argument to the install or the update command.

If some 3rd party package needs to be installed it’s enough to declare the package name and the package version in the require section. Composer also has a separate require command that allows adding a package to the corresponding section of the composer.json file automatically. For the Zend Framework module from the example above, the command to add a package will have the following structure:


composer require zendframework/zend-code ~2.4.6

The replace section contains a packages list and their versions that are already present in the current project or package out of the box. So, Composer checks the packages list in this section and does not look for the listed ones in the remote repositories, if some from this list are required by other packages.

The extra section contains an arbitrary data that is accessible to packages by using the following command:


$composer->getPackage()->getExtra()

Where $composer is an instance of Composer PHP object.

The autoload and autoload-dev sections are responsible for autoloading configuration of production and development environments correspondingly. The package manager uses its own autoloader to make a connection between all packages in the system.

The minimum-stability and prefer-stable parameters affect the version of the package that will be preferred when there is a version range set for a specific package in the require section instead of the exact version.

In Magento 2 Commerce the root composer.json file has some differences because of the installation particularities. The main difference is there’s no replace section. All packages are registered in the meta-package called magento/product-enterprise-edition. The package contains require section with all Magento components. The remote source of some packages is also different.

There’s one more section called repositories with only one record:


"repositories": [
        {
            "type": "composer",
            "url": "https://repo.magento.com/"
        }
    ]

This section contains a list of repositories where Composer will look for packages. In Magento 2 Open Source we don’t have such section. If the repositories section is not set for a project, Composer will look for packages in the default Composer repository – https://packagist.org/

The packages listed in the composer.json file usually contain a parameter called type, which is used for custom installation logic of a package. The default package type is library. For library packages, Composer simply copies the files to the vendor directory. In Magento 2 there are 5 custom platform-related packages types:

  • magento2-module
  • magento2-theme
  • magento2-library
  • magento2-language
  • magento2-component

All these packages are processed by Magento Composer Installer extension that comes with Magento 2 out of the box. The extension is a Composer plugin that provides the installer for Magento 2 packages listed above. The installer is implemented as \MagentoHackathon\Composer\Magento\Plugin class and contains logic for processing different packages types.

Basically, the main difference between package types is the installation directory. You can find the list of installation directories for every package in the MagentoHackathon\Composer\Magento\PackageTypes class.

I hope that the information in this post will help you understand the Composer processes and configuration in the scope of Magento 2 better and avoid common mistakes that lead to spending hours in an attempt to find out what goes wrong with that “unpredictable” Composer. Thanks for reading.