Composer vs. npm: managing dependencies in PHP and JavaScript

As developers, we rarely build everything from scratch. We usually rely on third-party libraries and frameworks, pulling them in as dependencies to accelerate development. But managing these dependencies – ensuring compatibility, resolving conflicts, and keeping things updated – can quickly become a headache. Hence, the need for package managers. In the PHP world, Composer is the goto package manager, while it is npm (or its alternatives like Yarn and pnpm) for the JavaScript ecosystem. Bun is also beginning to be a worthy contender in this space.

In this article, we will touch on Composer and npm, explore how to install, update, and specify package versions, ensuring your projects remain stable and secure. You'll learn the nuances of each tool and gain a deeper understanding of how they handle version constraints. By the end of this, managing dependencies should feel much less like a chore.

Composer: PHP's Main Dependency Manager

Composer centralizes package discovery (via Packagist) and automates installation and updates. Let's look at the key aspects.

Installing and Updating Packages

To add a new package (let's say Monolog for logging), you use the require command:

composer require monolog/monolog

This does two crucial things:

  1. It downloads Monolog and its dependencies into the project's vendor directory.
  2. It updates composer.json (which lists your project's direct dependencies) and composer.lock (which pins the exact versions of all installed packages, including dependencies of dependencies).

To update packages, you have a couple of options. composer update will update all packages to the latest versions allowed by your version constraints in composer.json. You can also update packages individually, like so:

composer update monolog/monolog
Databases in VS Code? Get DevDb

This gives you more control and reduces the risk of unexpected breakages due to simultaneous updates of multiple packages.

Version Constraints: Staying Compatible

Composer uses semantic versioning (SemVer: MAJOR.MINOR.PATCH). You specify version constraints in composer.json to control which versions of a package are acceptable. Here are some common examples:

{
    "require": {
        "monolog/monolog": "^2.0",      // Any version >= 2.0.0 and < 3.0.0 (most common)
        "guzzlehttp/guzzle": "~7.4",    // Any version >= 7.4.0 and < 7.5.0
        "symfony/console": "5.4.*",    // Any version matching 5.4.x
        "league/flysystem": "3.0.16", // Exactly 3.0.16
        "nesbot/carbon": ">2.0, <3.0" //greater than 2.0 but less than 3.0
    }
}
  • ^ (Caret): Allows updates to minor and patch versions, but not major versions (recommended for most dependencies).
  • ~ (Tilde): Allows patch-level updates if a minor version is specified; allows minor-level updates if only a major version is specified.
  • * (Wildcard): Matches any version. Useful for specific parts of a version number.
  • Exact version: Pins to a specific release. Use with caution, as you won't get any bug fixes or security updates.
  • Comparison operators. (>, >=, <, <=, !=)

Minimum Stability

By default, Composer defaults to stable package releases. If you need to use a development version (e.g., a release candidate) of a certain package, you can adjust the minimum-stability setting in composer.json:

{
    "minimum-stability": "dev",
    "prefer-stable": true
}

prefer-stable: true instructs Composer to prefer stable versions, if available, when resolving dependencies.

minimum-stability and prefer-stable determine which package versions can be installed.

Key Differences
  1. minimum-stability: Defines the lowest acceptable stability level for packages (dev, alpha, beta, RC, stable).
  2. prefer-stable: When true, Composer prefers stable versions if available, even if minimum-stability allows lower versions.
How it works:
  • Allows dev versions (since minimum-stability is dev).
  • Prefers stable versions when available (because prefer-stable is true).
  • If a package has both 1.0.0 (stable) and 1.0.1-beta, Composer picks 1.0.0 (stable).
  • If only dev versions exist, Composer installs the dev version.
Example Scenario
{
    "require": {
        "vendor/package": "*"
    }
}
  • If vendor/package has:
    • 1.0.0 (stable)
    • 1.1.0-beta
    • dev-master
  • Result: 1.0.0 (stable) is chosen.

If no stable version exists, the most recent dev version is installed.

Installing a specific version

If you need a specific version of a package, you can use the version constraint:

composer require monolog/monolog:2.9.2

npm: JavaScript's Package Manager

npm (Node Package Manager) is the default package manager for Node.js. It's used for both front-end and back-end JavaScript projects. While it shares many conceptual similarities with Composer, there are some key differences in its approach.

Installing and Updating

To add a package (e.g., lodash), you use npm install:

npm install lodash

Like Composer, this downloads the package (into node_modules) and updates package.json (your project's dependencies) and package-lock.json (the exact version lockfile). npm also supports npm install <package>@<version> for installing specific versions.

To update, you use npm update:

npm update lodash  # Update only lodash
npm update         # Update all packages

Similar to Composer, npm update respects the version constraints defined in package.json.

Version Constraints in npm

npm also uses SemVer, and the version constraint syntax is very similar to Composer's:

{
  "dependencies": {
    "lodash": "^4.17.21",      // Any version >= 4.17.21 and < 5.0.0
    "react": "~18.2.0",      // Any version >= 18.2.0 and < 18.3.0
    "axios": "1.6.7",      // Exactly 1.6.7
    "express": ">4.0.0"
  }
}

The caret (^) and tilde (~) behave almost identically to Composer. The main difference is that npm, by default, creates entries with the caret (^) when you npm install a new package, encouraging updates within the same major version.

Minimum stability

Since npm primarily sources its packages from the public npm registry which does not make any stability distinctions. npm does not have the concept of minimum-stability setting. Instead, npm uses tags to manage different release channels. By default, npm installs the latest tag. You can install other versions using tags:

npm install lodash@latest
npm install lodash@next

Installing a specific Version

You can use comparison operators similar to that of composer in npm:

npm install [email protected]

Composer vs. npm: Nuances and Similarities

Both Composer and npm achieve the same fundamental goal: managing external code dependencies within a project. Here's a comparison:

Feature Composer npm
Language PHP JavaScript (Node.js, browser)
Package Registry Packagist (packagist.org) npm Registry (npmjs.com)
Lockfile composer.lock package-lock.json
Versioning Semantic Versioning (SemVer) Semantic Versioning (SemVer)
Configuration composer.json package.json
Package Folder vendor node_modules
Version/tag Indicator Character : @

Conclusion

Understanding how to effectively use Composer and npm is very important for modern PHP and JavaScript development, respectively. By mastering the commands for installing, updating, and specifying dependencies, you can ensure your projects remain stable, secure, and easy to maintain. Ensure to always use version constraints when necessary in order to balance getting updates with avoiding breaking changes.

Wanna chat about what you just read, or anything at all? Click here to tweet at me on 𝕏