Composer and Package Management In Depth

Course 3 · Ch 1
Composer and Package Management In Depth
Beyond autoloading — version constraints, composer.lock, scripts, and managing a real project's dependencies

Chapter 6 of Intermediate introduced Composer purely for PSR-4 autoloading. Composer's actual core purpose is dependency management — installing, updating, and constraining the versions of third-party packages a project relies on. This chapter covers that side properly.

composer require — Installing a Real Package

$ composer require monolog/monolog # downloads the package into vendor/, adds it to composer.json, # and updates composer.lock

This single command does three things: downloads monolog/monolog and its own dependencies into vendor/, records the package in composer.json under "require", and writes the exact installed versions to composer.lock.

Semantic Versioning — How Package Versions Are Structured

2 . 4 . 1 MAJOR MINOR PATCH
MAJOR: breaking changes · MINOR: new features, backward-compatible · PATCH: bug fixes only

Semantic versioning ("SemVer") gives every package version a predictable meaning, which is what makes version constraints in composer.json actually useful rather than arbitrary.

Version Constraints

ConstraintMeaning
^2.4.12.4.1 or higher, but below 3.0.0 (no breaking changes allowed)
~2.4.12.4.1 or higher, but below 2.5.0 (patch updates only)
2.4.*Any 2.4.x version
>=2.0Any version 2.0 or above, with no upper limit
2.4.1That exact version only — rarely used, very restrictive
^ (caret) is the standard, recommended default
It allows genuinely useful updates (new features, bug fixes) to be installed automatically, while trusting SemVer's promise that a MAJOR version bump is the only kind of change allowed to break compatibility — and that kind of update is deliberately excluded.

composer.lock — Why It Matters, and Why It's Committed to Git

$ composer install # if composer.lock exists, installs the EXACT versions it specifies # (ignores composer.json's looser constraints entirely for this step) $ composer update # re-resolves composer.json's constraints to the newest allowed versions, # and REWRITES composer.lock to match
Unlike vendor/, composer.lock SHOULD be committed to git — the opposite rule from vendor/
Without it, two developers running composer install on the same composer.json could end up with subtly different package versions (anything matching the constraints, but not necessarily identical) — a classic source of "works on my machine" bugs. Committing composer.lock guarantees everyone, including a production server, installs the exact same versions.

require vs require-dev — Separating Real Dependencies From Tooling

$ composer require --dev phpunit/phpunit # installed under "require-dev" in composer.json — testing tools, # not needed to actually RUN the application in production
composer.json (excerpt)
{ "require": { "monolog/monolog": "^2.4" }, "require-dev": { "phpunit/phpunit": "^10.0" } }

A production deployment typically runs composer install --no-dev, skipping everything in require-dev entirely — testing frameworks (covered properly in Chapter 4) and similar developer-only tools have no reason to be installed on a live server.

Composer Scripts — Shortcuts for Common Commands

{ "scripts": { "test": "phpunit", "lint": "phpcs src/" } }
$ composer test # runs "phpunit" — a memorable, project-specific shortcut

The "scripts" section turns long or easily-forgotten commands into short, consistent ones that every contributor to a project can rely on, regardless of how the underlying tool is actually invoked.

Checking for Vulnerable Dependencies

$ composer audit # checks installed packages against known security advisories
Outdated dependencies are a genuine, common source of real vulnerabilities
A project can be written with perfect security practices and still be vulnerable purely because of an old version of a third-party library it depends on — running composer audit periodically (and especially before a deployment) is good practice, not paranoia.

Coding Challenges

Challenge 1

For each of these version constraints, write out in your own words exactly which versions would be considered acceptable: ^1.2.0, ~1.2.0, 1.2.*. Then explain which one you'd choose for a new project dependency, and why.

📄 View solution
Challenge 2

Write a composer.json with both a "require" entry for a hypothetical package "acme/mailer" version ^3.0, and a "require-dev" entry for "phpunit/phpunit" version ^10.0. Add a "scripts" section with a "test" shortcut running phpunit. Explain in a comment what command would install only the production dependencies, skipping require-dev entirely.

📄 View solution
Challenge 3

Explain, in your own words and in detail, the practical difference between running "composer install" and "composer update" when a composer.lock file already exists — including which one a CI/CD pipeline or production deployment should normally use, and why getting this wrong could cause a "works on my machine" bug.

📄 View solution

Chapter 1 Quick Reference

  • composer require pkg/name — installs a package, updates composer.json and composer.lock
  • SemVer: MAJOR.MINOR.PATCH — breaking / new feature / bug fix, by convention
  • ^ (caret) — recommended default constraint; allows non-breaking updates only
  • composer.lock — commit this to git (unlike vendor/); guarantees identical installed versions everywhere
  • composer install — installs exact locked versions; composer update — re-resolves and rewrites the lock file
  • require-dev / --no-dev — separates dev-only tooling (tests, linters) from real production dependencies
  • "scripts" in composer.json — memorable shortcuts for common project commands
  • composer audit — checks installed packages against known security advisories
  • Next chapter: design patterns in PHP — Singleton, Factory, Strategy, Repository