09 · Composer & Package Basics¶
🎥 Video walkthrough¶
Real PHP projects rarely write everything from scratch — they pull in
well-tested libraries for logging, HTTP requests, date handling, and more.
Composer is PHP's dependency manager: it downloads the packages your
project needs, tracks exact versions, and generates an autoloader so you can
use classes from those packages without manually require-ing every file.
Why Composer matters¶
Without a dependency manager you'd have to manually download library code,
track which version you have, resolve conflicts between libraries that need
different versions of a shared dependency, and hand-write require
statements for every class file. Composer automates all of that from a single
composer.json manifest.
Installing Composer¶
Composer is a separate command-line tool, not part of the PHP install itself.
# macOS / Linux -- one common way, via Homebrew
brew install composer
# Verify it installed
composer --version
# Composer version 2.7.x ...
(On Windows, the official installer is Composer-Setup.exe from
getcomposer.org; on Linux without Homebrew, the site provides a shell script
installer.)
composer init — starting a new project¶
composer init walks you through an interactive wizard (package name,
description, author, license, dependencies) and writes a composer.json
file. You can also just write composer.json by hand for a simple project:
{
"name": "acme/my-project",
"description": "A small demo project",
"type": "project",
"require": {
"php": ">=8.2"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Anatomy of composer.json¶
| Key | Purpose |
|---|---|
name |
vendor/package identifier (lowercase, slash-separated) |
description |
Short summary of the project |
require |
Production dependencies, with version constraints |
require-dev |
Dependencies only needed for testing/development |
autoload |
Maps namespaces to directories (PSR-4) so your own classes autoload too |
scripts |
Named shell commands you can run via composer run-script |
Requiring a package¶
This does three things: downloads monolog/monolog (and its own
dependencies) into vendor/, adds an entry to composer.json's require
section, and records the exact resolved versions in composer.lock so every
machine that runs composer install gets identical versions.
Semantic versioning constraints¶
Composer version constraints follow semantic versioning (MAJOR.MINOR.PATCH):
a major bump means breaking changes, minor adds features compatibly, patch
fixes bugs compatibly.
| Constraint | Meaning |
|---|---|
"3.7.0" |
Exactly this version |
"^3.7" |
>=3.7.0 <4.0.0 — allow minor/patch upgrades, not a new major |
"~3.7" |
>=3.7.0 <3.8.0 — allow patch upgrades only |
">=3.0" |
Any version 3.0 or newer, including future majors |
"3.7.\*" |
Any patch version within 3.7 |
^ (caret) is the most common default — it lets composer update pull in
bug fixes and new features without risking a breaking major-version change.
The vendor/ directory and the autoloader¶
After composer require (or composer install), Composer creates a
vendor/ directory containing all downloaded packages plus a generated
autoloader at vendor/autoload.php. Including that one file gives you access
to every installed package's classes — and your own PSR-4-mapped classes too
— without a single manual require.
<?php
// index.php
require 'vendor/autoload.php'; // one line, wires up every class in vendor/ (and your own, if PSR-4 mapped)
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger channel named "app"
$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::DEBUG));
$log->info('Application started');
$log->warning('Disk space running low', ['freeMb' => 512]);
$log->error('Failed to connect to payment gateway');
echo "Check app.log for the log output.\n";
Running php index.php writes structured log lines to app.log, something
like:
[2026-07-18T10:15:32+00:00] app.INFO: Application started [] []
[2026-07-18T10:15:32+00:00] app.WARNING: Disk space running low {"freeMb":512} []
[2026-07-18T10:15:32+00:00] app.ERROR: Failed to connect to payment gateway [] []
vendor/autoload.php should always be .gitignored along with vendor/
itself — anyone cloning the project runs composer install (which reads
composer.lock) to regenerate it identically, so it never needs to be
committed.
Autoloading your own classes with PSR-4¶
Composer's autoloader isn't just for third-party packages — map your own namespace to a directory and Composer will autoload your classes too:
<?php
// src/Greeter.php
namespace App;
class Greeter
{
public function greet(string $name): string
{
return "Hello, $name!";
}
}
<?php
// index.php
require 'vendor/autoload.php';
use App\Greeter;
$greeter = new Greeter();
echo $greeter->greet('World'); // Hello, World!
After adding or changing an autoload mapping, run composer dump-autoload
to regenerate the class map.
Composer commands cheat sheet¶
| Command | Purpose |
|---|---|
composer init |
Interactively create a new composer.json |
composer require <pkg> |
Add and install a production dependency |
composer require --dev <pkg> |
Add and install a dev-only dependency |
composer install |
Install exact versions from composer.lock |
composer update |
Resolve and install the newest versions allowed by constraints |
composer dump-autoload |
Regenerate the autoloader (e.g. after adding a new class) |
composer show |
List installed packages and their versions |
How It Actually Works¶
Composer's autoloader isn't magic — require 'vendor/autoload.php' registers a callback with spl_autoload_register(), and the engine only invokes that callback the first time an unresolved class name is referenced (in a new, extends, instanceof, or static call). At that moment PHP calls your registered function with the class name as a string, and Composer's generated autoloader (built from the psr-4 mapping in composer.json) turns the namespace into a file path and does a plain require. This lazy, on-demand loading is why adding an unused require for a class you never instantiate costs nothing beyond parsing the mapping array — the actual class file is never opened. composer.json version constraints (^, ~) aren't enforced at runtime at all; they're resolved once, when you run composer install or update, by Composer's own dependency-resolution algorithm (a SAT-style constraint solver) which picks the highest set of package versions satisfying every constraint and locks that exact resolution into composer.lock — after that, PHP itself has no idea what "^2.0" meant, it just requires whatever file the autoloader map points at.
🔀 See this in another language¶
- Swift — Closures Intro
- SQL — Inserting, Updating, Deleting Data
- TypeScript — Modules & tsconfig Deep Dive
Exercise¶
Create a new folder, run composer init (or hand-write a minimal
composer.json targeting php >= 8.2), then run composer require
ramsey/uuid. Write a small index.php that requires vendor/autoload.php,
generates a UUID with Ramsey\Uuid\Uuid::uuid4(), and prints it. Confirm
vendor/ and composer.lock were created, and note which section of
composer.json now lists ramsey/uuid as a dependency.