Composer and Package Management In Depth
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
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
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
| Constraint | Meaning |
|---|---|
| ^2.4.1 | 2.4.1 or higher, but below 3.0.0 (no breaking changes allowed) |
| ~2.4.1 | 2.4.1 or higher, but below 2.5.0 (patch updates only) |
| 2.4.* | Any 2.4.x version |
| >=2.0 | Any version 2.0 or above, with no upper limit |
| 2.4.1 | That exact version only — rarely used, very restrictive |
composer.lock — Why It Matters, and Why It's Committed to Git
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
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
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 periodically (and especially before a deployment) is good practice, not paranoia.
Coding Challenges
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.
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 solutionExplain, 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 solutionChapter 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