php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

Getting Out of PHP Dependency Hell With Composer

Posted by on January 24, 2023

In the darker days of web application development, we essentially had two options for working with dependencies. The first is that we could put all the dependencies in a directory of our application and use them from there. This worked but could cause our application to balloon in size. The other option was to rely on shared dependencies installed on the server using something like Pear. Because we didn’t control this, we would occasionally have to debug problems because someone installed a different version of the library than what the application was developed against. And lord help you if you had your application installed on a server with other applications because each might need a different version of the same package.

This was such a common occurrence it was called dependency hell.

Thankfully, as a community, we no longer have to worry about dependency hell because we can use Composer to solve all of these problems for us. In this article, we’ll discuss what Composer is, some of its features, and how to integrate it with your project today.

What is Composer

Composer is a tool for dependency management in PHP. It allows us to declare the dependencies that our project requires and it will automatically determine all the dependencies of our dependencies. It manages these dependencies on a per-project basis, so we severely reduce the chance we’re going to have a dependency hell situation on our hands.

Once we’ve installed our dependencies locally, Composer will lock those dependencies into a specific version so we can install the same versions on any number of computers easily.

When we need to update a package, a simple command will upgrade all the dependencies.

Composer also provides a built-in autoloader, so we don’t need to `require_once()` all of the classes we need for each request.

Installing

Composer itself comes as a self-contained PHAR file so it’s very portable. We just need one of the supported versions of PHP, and we can easily add it to most systems.

Exact installing instructions are available on https://getcomposer.org/. There are two options for how we can install Composer. The first is that we can install the PHAR file into our current project. The other option is that we can install it globally into our system’s path. Locally is nice because it can then be included in our source code management (SCM) and be at the same level for all developers and servers. Globally is nice because it reduces the total size of our repository and it’s easily accessible anywhere in our system.

I prefer including it as part of the repository so it stays consistent across all the different systems we have to maintain. For the examples in this article, we’ll assume composer.phar is located at the root of our project directory.

Adding Our First Dependency

Let’s create a project and use Composer to install a couple of dependencies.

Composer uses package names to uniquely identify a dependency. It’s comprised of a vendor name and a project name separated by a slash. The vendor name is part of the schema so we can have multiple packages with the same name.

Let’s add PHPUnit to our project so we can start on the right foot doing Test-driven Development.

To install this, we’re going to use the `require` command to tell Composer we’re going to add a dependency to our project. Then use the `–dev` switch to tell it that we’re installing a development-only dependency, so it doesn’t get installed in our production environments. This isn’t strictly a requirement, but it can reduce the attack surface of our servers. Next, we’re going to pass the vendor and package name. In this case, it’s `phpunit/phpunit`, and then finally, we’re going to specify a Semantic Version (SemVer) string to indicate what version we want to be used. You should generally be using a carrot at the start as that will keep you inside the same major version.

php composer.phar require --dev phpunit/phpunit ^9

After we hit enter, Composer will perform several actions:

1. Adding phpunit/phpunit to our “composer.json” file. Creating the file if it doesn’t exist.
2. Determining the correct version of `phpunit/phpunit` to install along with its dependencies
3. Writing this information to the “composer.lock” file
4. Downloading and installing the libraries to the project’s “vendor” directory
5. Creating an autoload.php file

The “composer.json” and “composer.lock” files should also be included in your SCM but not the “vendor” directory. The “vendor” directory will be recreated using the “composer.lock” file’s information. It’s important to know that the “composer.lock” file contains the versions of the packages that any other computer will use for our project and not the “composer.json”. We can update the “composer.json” all we want, but unless we also update the lock file, it won’t do anything.

Another thing to mention before we move on is that inside the vendor directory are several important things. The first is the “autoload.php” which we can include in our project to provide autoloading of the files our project needs as they’re requested (more on that later). The next is the “bin” directory. This contains the executable versions of any dependencies we’ve downloaded from using Composer. For example, the `phpunit` executable is located here. It also contains directories for each vendor we’re pulling libraries for. Inside those directories will be more directories for each project from that vendor.

Adding Our Second Dependency

Let’s add our second dependency because we don’t yet have a package that will be installed in our production environment. Let’s install monolog to our project so we can log some data.

php composer.phar require monolog/monolog ^3

Notice this command doesn’t have the `–dev` argument. This indicates it’s not a development-only requirement. Once we’ve run that command, let’s look at our “composer.json” file so we can see how it was affected.

{
"require-dev": {
"phpunit/phpunit": "^9"
},
"require": {
"monolog/monolog": "^3"
}
}

See how we have separate keys for each type of dependency?

Installing Our Project on Another Computer

It’s time to move this project into production. I spun up a Cloudways VM with PHP 8.1 installed and cloned the project to the new VM.

Now what we’re going to do is use the `install` command to install our dependencies. There are lots of options for this.

To start, we’re going to just use the `install` command without any options. In this process, it uses the “composer.lock” file to download the correct versions of all our dependencies to the server.

php composer.phar install

Unfortunately, this installed PHPUnit. This is less than ideal from a security standpoint. PHPUnit isn’t a problem, but we might have a debug bar installed that we don’t want in production.

To prevent this we’ll use the `–no-dev` switch to only install items in the “require” key in our JSON file.

In production, we should also use the `–optimize-autoloader` switch to have Composer optimize its autoloader for a production environment. This means it creates a class map (“vendor/composer/autoload_classmap.php”) between the files in our system and their name so it doesn’t have to search the file system when we need a class loaded.

php composer.phar install --no-dev --optimize-autoloader

This time no PHPUnit.

Versioning/Updating

Now that we have Composer tracking our dependencies, it’s important to talk about updating those dependencies. Our “composer.lock” file is going to keep us locked (see why it’s called that) to a specific version of each dependency. This is great for consistency but bad because we’re not getting security or bug fixes. Thankfully Composer has got our back.

When we specified our SemVer string for each of our packages, we were telling Composer what version range was acceptable to use. Ideally, we should be locked into the current major version of each package because packages shouldn’t be breaking anything inside of a major version so it should be a painless upgrade to the most current version. That doesn’t always happen so it’s important to test upgrades before they’re put on the production servers.

To upgrade our dependencies, we’ll be using the `update` command. This will cause Composer to check our dependencies for updates.

php composer.phar update

After this process has been completed, the “composer.lock” file will be updated to reflect the new versions, which we can then push to our SCM and then download the changes to all of the servers may be using a tool like Deployer

On projects that are in active development, I run this command at least once a month to make sure we’re not slipping too far behind and keeping up to date on security patches. I’ll generally run it on the first working day of the month as part of my start-of-month checklist. If I hear about a security issue in the PHP ecosystem, I’ll run it part way through the month as well.

Packagist

You might be wondering how Composer knows how to find our dependencies. The answer to that is simple, it uses as a lookup service to know about the packages that can be used. Packagist is the main Composer repository and it allows library developers to submit their packages for inclusion in the repository. Then Packagist tells Composer where to find the source code.

In our next article, we’ll create a package and get it listed on Packagist. Make sure you subscribe so you’re notified when it’s available.

Autoloading

It’s best practice to create a lot of small classes for our application so we can keep our application speedy and easy to maintain. In the days before autoloading was common, we would have to manually add `require_once()` calls for all of the classes we needed. Thankfully, PHP 5.1 added support to declare a function to automatically load files as classes were requested. Composer comes with an autoloader so we don’t need to write one ourselves.

To use it we just need to add a `require ‘/vendor/autoload.php’;` to our application and it will do the rest for the files located in our `vendor` directory.

For our files, we’ll have to tell it how to find our classes. This is done by adding an “autoload” object to our “composer.json” file that tells the autoloader how to map our classes to the directory structure. Generally, our Package name gets mapped to the “src” directory and then each directory is its own namespace.

{
"autoload": {
"psr-4": {"ScottsValueObjects\\": "src/"}
}
}

Now we can attempt to use a class named `ScottValueObjects\Name\FirstName` and the autoloader will attempt to load the file “src/Name/FirstName.php” for this class.

What You Need to Know

– Composer is a powerful tool in PHP ecosystem
– Helps maintain dependencies
– Provides autoloading

php[architect] is an industry-leading publication focused on the PHP language. We are for developers, written by developers. Publishing great PHP content since 2002 and dedicated to providing educational material to the PHP programming community.

Subscribe today at https://phpa.me/signup
Twitter: @phparch
Mastodon: phparch.social@editor
Facebook: https://phpa.me/facebook
LinkedIn: https://phpa.me/linkedin

This video is sponsored by Honeybadger


Tags: ,
 

Leave a comment

Use the form below to leave a comment: