Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Composer for the Rest of Us
Learn about how you can leverage Composer like a pro to manage & install your dependencies in PHP-based projects
Composer is a tool for dependency management in PHP. This means you use it to declare the PHP packages your project depends on and it will manage them for you.
Tools like Composer can be a little intimidating when you first approach them, but like everything else, understanding how it works will get you a long way to a peaceful & productive relationship with Composer.
Much like npm in the Javascript world, Composer allows you to:
- List the dependencies of your project in a file (composer.json)
- Install versions of these dependencies that are all compatible with each other
- Update all of these dependencies in an automated yet structured way
Composer also generates autoload information, which allows classes from disparate packages to be used easily.
Despite their quirks, package managers are critical for developing modern applications, because of the mix of packages they depend on.
In this article, you’ll learn the important fundamentals of Composer, as well as a number of pro tips & useful Composer commands you can use every day.
“Learning is a treasure that will follow its owner everywhere.” — Chinese Proverb
It’s important to understand some of the fundamental concepts in Composer, so we can build upon them.
Link Composer Essentials
Let’s start with some definitions of important things in the Composer world:
- composer — a command line tool written in PHP that you need to either install locally, or run via Docker in order to use it.
- Packagist.org — the primary registry of PHP packages that are available to be managed by Composer.
- composer.json — the file where you list the PHP dependencies your project has, as well as other meta-information about your project (name, description, license, etc.)
- composer.lock — a file generated by Composer after it resolves all of your package dependencies, and installs them. It contains the exact versions of every package that is actually installed.
The composer.json file is the hub of the Composer world, so let’s have a look at an example of a simple composer.json file:
{
"name": "nystudio107/composer-example",
"description": "An example composer.json file",
"license": "MIT",
"authors": [
{
"name": "Andrew Welch",
"email": "andrew@nystduio107.com"
}
],
"require": {
"twig/twig": "^v3.0.0",
"vlucas/phpdotenv": "~5.4.0",
"elvanto/litemoji": "3.0.1"
}
}
The composer.json file is just a standard JSON that conforms to the composer schema, which defines all of the available fields and what they are used for.
You can create the composer.json file yourself in any text editor, or you can use the composer init CLI command to create it interactively for you.
Link The Require field
Let’s focus on the require field, because there are some critical things going on there:
"require": {
"twig/twig": "^v3.0.0",
"vlucas/phpdotenv": "~5.4.0",
"elvanto/litemoji": "3.0.1"
}
The require field in the composer.json file is where we specify the packages that are PHP dependencies that our project needs to work.
On the left is the package specifier, in vendor / name format, and on the right is the version constraint we want. Version constraints are a combination of starting package version numbers expressed as semvers (semantic versions) and operators that determine what that package can be updated to.
When Composer needs to install or update your project’s dependencies, it finds them by looking up the vendor/name on packagist.org (by default), and then uses the version constraint to find a version that matches your requirements.
It then resolves the requirements of all of the other packages that your project uses to arrive at the most up-to-date combination of packages that satisfy all of them.
Version numbers resolve to tagged releases of a package in their VCS repository
By default, Composer looks up the versions as tags from a package’s Version Control System (VCS) repository (usually GitHub).
So the version you ask for will resolve to a specific tagged release of the package. A tag just points to a specific VCS commit in the repository.
There is also a require-dev field that is functionally the same as the require field, except that dependencies listed here are intended to be installed only in your local development environment.
require-dev is for things like tests, debugging & profiling tools that you only need when developing your project.
Link Semver Versions
Since most PHP packages are using semver as a versioning methodology, let’s have a look at what that means.
Semver is important because it provides a standard that defines what the . delimited version numbers mean:
- operator — technically not part of semver, but determines the version constraint (see below)
- major version — incremented when the package makes incompatible API changes
- minor version — incremented when the package adds functionality in a backwards compatible manner
- patch version — incremented when the package makes backwards compatible bug fixes
The major version, minor version, and patch version are . delimited, and together form the semver.
Occasionally, you might see a version with a 4th version specifier, such as 4.2.0.1. Sometimes called a “hotfix” version, this isn’t strictly part of semver. But any version constraint that receives at least patch version updates (see below) will also get hotfix version updates.
Note that with Composer, some semvers have a v in them (v3.0.0) and some don’t (2.0.0). The v is an optional convention, that is stripped out by Composer before evaluating a semver, so you can use it or not.
Now let’s have a look at how semvers are used with operators to make a version constraint.
Link Version Constraints
The operator in a version constraint can be comparators like <, <=, > or >=, but typically you’ll use two special operators:
- ^ — the caret operator specifies that you want minor & patch version updates up to, but not including the next major version
- ~ — the tilde operator specifies that you want patch version updates up to, but not including the next minor version
If no operator is specified, then you will not get anything other than the exact version specified via the semver.
The idea is that you specify an initial package version, and use the operator to tell Composer the constraints it should use when updating that package for you.
So let’s look at the versions in our example composer.json file, and what they mean:
- "twig/twig": "^v3.0.0" — Composer will update the package to any version that is less than 4.x (the next major version), so you get all minor and patch version updates
- "vlucas/phpdotenv": "~5.4.0" — Composer will update the package to any version that is less than 5.5.x (the next minor version), so you get all patch version updates
- "elvanto/litemoji": "3.0.1" — Composer will always install exactly version 3.0.1 of this package, so you get no updates
In most projects, you’ll almost always use the ^ caret operator to specify your dependencies. This is because you want all backwards compatible minor & patch version updates, but don’t want Composer to update your packages to major versions that may have breaking changes.
In some cases, if you’re very conservative or a package isn’t strictly adhering to semver, you might use the ~ tilde operator to only get patch version updates.
A great interactive tool for learning version constraints interactively is the Packagist Semver Checker:
You can enter real-world packages and version constraints, and visually see what matches.
Link Advanced Version Constraints
Most people can probably skip over this section, unless you run into situations where you have some edge cases to solve in terms of version constraints. But it’s presented here for those who live on the edge.
If you want to go wild, and specify every version after the initial version, you can do this:
- "twig/twig": ">=v3.0.0" — Composer will update the package to any version that is greater than or equal to 3.0.0. This is typically not a great idea, because it means you will get all major version updates (which may contain breaking changes)
You can also have multiple version constraints for a given package, delimited by || for a logical or. This is useful if you can use multiple versions of a package, because other packages may have more specific constraints:
- "twig/twig": "^v3.0.0 || ^v4.0.0" — Composer will update the package to any version that is less than 5.x (all 3.x and 4.x versions)
You can also have multiple version constraints delimited by a space or a , for a logical and:
- "twig/twig": ">=v3.0.0,<3.5.0" — Composer will update the package to any version that is greater than or equal to 3.0.0 and less than 3.5 . ">=v3.0.0 <3.5.0" is another way to write the same thing
As mentioned earlier, versions resolve to tagged releases in a package’s VCS repository. A tag just points to a specific VCS commit in the repository.
If instead you need to use a branch of a repository (maybe there is no tagged release), you can specify that branch by adding the dev- prefix to the branch name:
- "craftcms/cms": "dev-develop" — Composer will clone the repository down, and check out the develop branch
If you need to use a branch that has a name that looks like a version, you specify that branch by adding a -dev suffix instead:
- "craftcms/cms": "5.0-dev" — Composer will clone the repository down, and check out the 5.0 branch
If you need to use a branch of a package’s repository, but other dependencies also require the same package with a version constraint, you can use aliases with as:
- "nystudio107/craft-seomatic": "dev-develop-v4 as 4.0.9" — Composer will clone the repository down, and check out the develop-v4 branch, but alias it to 4.0.9 so that any other package that requires "nystudio107/craft-seomatic": "^4.0.0" will still be happy
See Writing Version Constraints for even more examples of what you can do with version constraints.
Link Adding Packages
To add a package to your project, you can just open up your text editor, and manually add a package and a version
"require": {
"twig/twig": "^v3.0.0",
"vlucas/phpdotenv": "~5.4.0",
"elvanto/litemoji": "3.0.1",
"guzzlehttp/guzzle": "^7.0.0"
}
However, this requires that you know both the vendor/name of the package, and also the right version constraint to put in.
Instead, you can use the Composer CLI to add the package with composer require:
composer require guzzlehttp/guzzle
It will find a version of the package that is the latest version that will work with all of your other project packages, and add it to the composer.json for you:
"guzzlehttp/guzzle": "^7.4",
It will set the version constraint to the latest minor version of the package that works with your dependencies, and use the ^ caret operator, so you get all minor & patch updates of the package.
If you don’t know the full vendor/name of the package, you can find it interactively by using composer require without any arguments:
Using the composer require command will also install the new package for you, and update any other packages as appropriate.
Link Updating Packages
The reason we went so in-depth on semvers and version constraints is that when you want to update your packages, you can just use the composer update CLI command:
composer update
Composer will then automatically update all of your packages to the latest versions that satisfy all of the version constraints in all of the packages.
You can also elect to update only an individual package by specifying it:
composer update guzzlehttp/guzzle
In either case, Composer then writes out the exact versions of all of the packages that end up getting installed to the composer.lock file.
A good way to think about the relationship between the composer.json file and the composer.lock file is this:
The composer.json file is what you order off of the menu, the composer.lock file is whatever ends up getting delivered to your table
Usually, you’ll only use composer update in local development. A typical workflow looks like this:
- Run composer update in local development to update your packages
- Test to ensure your project continues to work as expected
- Check the modified composer.lock file that is generated into your VCS repository
- Deploy that updated composer.lock file to production or CI pipeline
- On production or in your CI pipeline, run composer install
What composer install does is it installs the exact versions of all of your packages that are listed in the composer.lock file.
It doesn’t update anything, it installs the exact versions that you’ve tested and know work.
This is the same methodology you would use in a team environment. After updating packages as noted above, have the others on your team pull down the modified composer.lock file and run composer install.
This ensures that everyone is running the same exact package versions.
The actual command we typically use on production or CI pipeline is:
composer install --no-dev --no-progress --no-interaction --optimize-autoloader
Let’s break down what the flags mean:
- --no-dev — Skip installing packages listed in require-dev
- --no-progress.- Removes the progress display that can mess with some terminals or scripts which don’t handle backspace characters
- --no-interaction — Do not ask any interactive questions
- --optimize-autoloader — Convert PSR‑0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default
For an in-depth look at the composer.lock file, have a look at the article PHP: The Composer Lock File. For more on deployments, check out the Atomic Deployments without Tears article.
Link Useful Composer Commands
Here are some useful Composer CLI commands that will make your life easier:
- composer require — add a new package to your project’s composer.json file, and install it
- composer remove — remove an installed package from vendor/and your composer.json file
- composer update — update your project’s dependencies to the latest versions that satisfy all of the packages version constraints
- composer install — install the exact versions of the packages listed in the generated composer.lock file
- composer reinstall — uninstalls and reinstalls composer packages, for a clean install of the packages
- composer show -D — list all of your project’s direct dependencies and installed versions
- composer outdated -D — list all of your project’s direct dependencies that have newer updates available
- composer init — create a new composer.json file interactively
composer outdated -D is especially useful when determining when a project needs to be updated:
For a complete list of all of the available commands, check out the Composer CLI reference.
Link Platform Requirements
In addition to PHP packages, both the require and require-dev fields also support references to specific PHP versions and PHP extensions your project needs to run successfully:
"require": {
"php": ">=7.4.0",
"ext-mbstring": "*"
"twig/twig": "^v3.0.0",
"vlucas/phpdotenv": "~5.4.0",
"elvanto/litemoji": "3.0.1"
}
Both php and ext- requirements are special in that they don’t install anything. They just ensure that these minimum platform requirements are present before Composer will attempt to install your project’s dependencies.
- php is a special case that checks to ensure that the installed version of PHP matches at least the version constraint specified
- Anything prefixed with ext- refers to a PHP extension. The * in the version constraint is a wildcard which means any version is fine, as long as it is installed.
It’s important to specify any PHP extensions that your project relies on, because not all PHP installations may have the required extensions installed.
Not doing so means that it’s possible PHP could throw a runtime error trying to execute your project, if the requisite PHP extension isn’t present.
Link Config Field
There is also a config field in the composer.json file that allows you to specify global configuration settings that Composer should use:
"config": {
"platform": {
"php": "7.4",
"ext-mbstring": "1.0.1"
},
"allow-plugins": {
"craftcms/plugin-installer": true,
"yiisoft/yii2-composer": true
},
"optimize-autoloader": true,
"sort-packages": true
},
The platform field inside of the config field looks very similar to what we listed in the require field above in PLATFORM REQUIREMENTS, but it has a subtle difference.
It lets you specify fake platform requirements for PHP and PHP extensions. Typically this is done when environments have different versions of PHP installed, and you want to ensure that your packages are all installed with the appropriate version.
For example, let’s say have are running PHP 8.1 in our local development environment, but the production environment we deploy our project to is only running PHP 7.4.
In the platform field, we’d set "php": "7.4" to ensure that any packages that get installed locally will run on PHP 7.4, so they will work both in our local development environment, and also on production.
The allow-plugins field is required as of Composer 2.2.0 or later to list any Composer plugins that should be allowed to run when using Composer.
The optimize-autoloader field ensures that when packages are installed, the autoload classmap will be optimized. This is false by default, because it can take a little extra time to do, but it is recommended to be done on production installs, because it makes the runtime resolution of Composer packages faster.
The sort-packages field causes Composer to sort the packages in the require and require-dev fields alphabetically for you, to keep them neatly organized.
These are just a few config options you might be interested in using, or have seen out in the wild. For a complete list, check out the composer.json Config documentation.
Link Composer Scripts
Composer also has a scripts field in the composer.json file. Composer scripts are command-line executable commands that run at various stages in the Composer CLI lifecycle.
For instance, you might want to run something after a composer install or composer update happens. To do that, you’d add this to your composer.json:
"scripts": {
"post-install-cmd": "echo 'install finished'",
"post-update-cmd": "echo 'update finished'"
}
A great use-case for this is as part of a deployment process, to clear caches after updates are installed via composer install.
You can also define your own scripts, and run multiple CLI commands:
"scripts": {
"custom-script": [
"echo 'custom 1",
"@php some-script.php",
],
"prepostinstall-cmd": "echo 'install finished'",
"post-update-cmd": "echo 'update finished'"
}
The @php directive can be used to run other PHP scripts, and will resolve to whatever PHP process is currently being used.
Custom scripts can then be run via composer run custom-script, or they can be run by the special Composer lifecycle events:
"scripts": {
"custom-script": [
"echo 'custom 1",
"@php some-script.php",
],
"post-install-cmd": "echo 'install finished'",
"post-update-cmd": "@custom-script"
}
In the example above, "post-update-cmd": "@custom-script" will cause all of the CLI commands defined in custom-script to be run after a composer update is done.
Composer scripts can be a very handy way to package functionality along with your project, and ensure that scripts are run after Composer lifecycle events, regardless of the environment.
Link Working Locally with Path Repositories
We mentioned earlier that Composer by default looks up packages via Packagist by default when it tries to find and install your package dependencies.
However, what if you want to work on your own PHP package locally? You can tell Composer that you have another repository that you want it to look at when resolving packages, specifically a path repository by specifying it in your composer.json file:
"repositories": [
{
"type": "path",
"url": "/Users/andrew/webdev/craft_v4/craft-seomatic"
}
],
The type of the repository is a path, and the url is the path (which can be a relative or an absolute path) to the local directory on your computer where Composer can find the repository.
You can also use a wildcard * character when specifying your path repository:
"repositories": [
{
"type": "path",
"url": "/Users/andrew/webdev/craft_v4/*",
}
],
This will cause Composer to try to resolve all packages by looking for them in the path specified in the url field. If it fails to find the package there, it’ll fall back on looking for it on packagist.org.
Then you can require the PHP package you’re working on in your project’s composer.json file as you normally would, but work on it locally (typically the PHP package will be a separate git repository).
When Composer resolves a package from a local path repository, it will create a symlink in your project’s vendor/ directory, and everything will “just work”.
Path repositories take priority over remote VCS repositories and Packagist.
Link Using a Package from a Forked Repository
A scenario that unfortunately happens on occasion is that you have a PHP dependency that your project needs to function. Maybe it’s a plugin of some sort for a CMS, maybe it’s just a straight up PHP package.
But there’s a bug in the package that you need fixed.
You open up the package’s GitHub repository URL in your browser, and your blood runs cold. A cloud of dread forms around you. A bead of sweat glistens on your forehead. Your worst fears are realized:
The package you are having an issue with is abandoned, or at the very least doesn’t appear to be actively updated.
So let’s say you have some PHP chops or the bug fix is relatively simple, so you roll up your sleeves, you fork the repository on GitHub. You make the bug fix, and being a good citizen, you make a pull request so the package maintainer can use your bug fix, and everyone benefits.
Unfortunately, your client needs the fix to their project now. What you can do is tell Composer to use your fork of the package:
"repositories": [
{
"type": "vcs",
"url": "https://github.com/khalwat/craft-seomatic",
},
],
"require": {
"nystudio107/craft-seomatic": "dev-develop-v4 as 4.0.9"
}
We tell Composer to use the VCS repository we’ve specified when resolving packages, and then we alias the develop-v4 branch to version 4.0.9 of the package, and away we go!
If the maintainer of the original package ends up fixing the issue in the original package, no problem. Just remove the VCS repository and require alias from your composer.json.
For a deeper dive on this, Ben Croker wrote a more in-depth article Requiring a Forked Repo with Composer.
Link Compose a masterpiece
So that’s a brief rundown on what you need to know to use Composer with confidence.
There’s more to Composer than can be listed in any single article, but hopefully, this primer has given you what you need to make beautiful music together.
Indeed, the very name “Composer” is a double entendre that alludes to both composing music, and also composing multiple PHP packages together to create something larger than the sum of its parts.
And yes, the Composer logo is a conductor, and makes no sense. 🤔
For an example of using the Composer of CLI to update Craft CMS & plugins, check out the Updating Craft CMS without Headaches article.
If you run into issues with Composer, check out the Troubleshooting Composer Errors knowledge base article.