Composer And Packages
Modern PHP development relies heavily on reusable libraries and packages. Rather than writing every utility from scratch, you pull in well-tested, community-maintained code through Composer — PHP's dependency manager. Composer handles downloading packages, managing version constraints, and autoloading classes so your code stays clean and organised. Understanding Composer is essential for any PHP developer working on real-world projects.
What Is Composer?
Composer is a command-line tool that reads a composer.json file in your project root, resolves dependencies, and installs them into a vendor/ directory. It generates an autoloader so you can use any installed class without writing a single require statement yourself.
A minimal composer.json looks like this:
{
"name": "myapp/blog",
"require": {
"php": ">=8.1",
"ramsey/uuid": "^4.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Running composer install reads this file, downloads the packages listed under require, and writes a composer.lock file that pins exact versions for reproducible installs across environments.
The Autoloader
The most immediately useful thing Composer provides is its autoloader. After installation, you include a single file at the top of your entry point:
require __DIR__ . '/vendor/autoload.php';
From that point on, any class — whether from a package or your own src/ directory — is loaded automatically when first referenced. No more long chains of require_once calls scattered through your codebase.
Working With Packages
To demonstrate how packages work in practice, consider a common task: generating universally unique identifiers (UUIDs). Instead of implementing the UUID algorithm yourself, you install an existing package.
composer require ramsey/uuid
Once installed, you import and use the class:
<?php
// Simulating what the ramsey/uuid package provides
// In a real project this would be loaded via composer's autoloader
final class Uuid
{
private string $value;
private function __construct(string $value)
{
$this->value = $value;
}
public static function uuid4(): self
{
$data = random_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return new self(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
}
public function toString(): string
{
return $this->value;
}
public function __toString(): string
{
return $this->value;
}
}
$id = Uuid::uuid4();
echo "Generated UUID: " . $id . "\n";
$orderId = Uuid::uuid4();
$userId = Uuid::uuid4();
echo "Order ID: {$orderId}\n";
echo "User ID: {$userId}\n";
echo "Are they equal? " . ($orderId->toString() === $userId->toString() ? "yes" : "no") . "\n";
Each call produces a globally unique identifier. This is the kind of utility that would take significant effort to write and test correctly yourself.
Version Constraints
Composer uses semantic versioning and a range of constraint operators to describe which package versions your project accepts.
| Constraint | Meaning |
|---|---|
^4.0 | >=4.0.0 and <5.0.0 (same major) |
~4.2 | >=4.2.0 and <5.0.0 |
4.1.3 | Exactly 4.1.3 |
>=4.0 <5.0 | Explicit range |
The caret (^) is the most common choice. It allows minor and patch updates — which should be backwards-compatible — while preventing a new major version from silently breaking your code.
<?php
// Simulating version constraint logic to illustrate how Composer thinks about versions
function parseVersion(string $version): array
{
$parts = explode('.', ltrim($version, 'v'));
return [
'major' => (int)($parts[0] ?? 0),
'minor' => (int)($parts[1] ?? 0),
'patch' => (int)($parts[2] ?? 0),
];
}
function satisfiesCaretConstraint(string $constraint, string $version): bool
{
$required = parseVersion(ltrim($constraint, '^'));
$actual = parseVersion($version);
if ($actual['major'] !== $required['major']) {
return false;
}
if ($actual['minor'] < $required['minor']) {
return false;
}
if ($actual['minor'] === $required['minor'] && $actual['patch'] < $required['patch']) {
return false;
}
return true;
}
$constraint = '^4.2.0';
$versions = ['3.9.0', '4.1.5', '4.2.0', '4.2.3', '4.5.1', '5.0.0'];
echo "Constraint: {$constraint}\n\n";
foreach ($versions as $v) {
$ok = satisfiesCaretConstraint($constraint, $v);
echo " {$v}: " . ($ok ? "allowed" : "rejected") . "\n";
}
Running this shows exactly which versions pass the ^4.2.0 constraint — only versions in the 4.x range at or above 4.2.0.
PSR-4 Autoloading
Composer's autoloader follows the PSR-4 standard, which maps namespace prefixes to directory paths. Given this configuration:
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
A file at src/Services/Mailer.php is automatically available as App\Services\Mailer. The directory structure mirrors the namespace hierarchy. After adding or changing autoload configuration, regenerate the autoloader:
composer dump-autoload
<?php
// Demonstrating PSR-4 namespace conventions without a real filesystem
// Imagine these files exist:
// src/Models/User.php -> namespace App\Models; class User
// src/Services/Auth.php -> namespace App\Services; class Auth
// With PSR-4, you just use them:
// use App\Models\User;
// use App\Services\Auth;
// Simulating the classes here to show the pattern
namespace App\Models {
class User
{
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email,
) {}
}
}
namespace App\Services {
use App\Models\User;
class UserRepository
{
private array $store = [];
public function save(User $user): void
{
$this->store[$user->id] = $user;
}
public function find(int $id): ?User
{
return $this->store[$id] ?? null;
}
public function all(): array
{
return array_values($this->store);
}
}
}
namespace {
use App\Models\User;
use App\Services\UserRepository;
$repo = new UserRepository();
$repo->save(new User(1, 'Alice', 'alice@example.com'));
$repo->save(new User(2, 'Bob', 'bob@example.com'));
$repo->save(new User(3, 'Carol', 'carol@example.com'));
$user = $repo->find(2);
echo "Found: {$user->name} ({$user->email})\n";
echo "\nAll users:\n";
foreach ($repo->all() as $u) {
echo " [{$u->id}] {$u->name}\n";
}
}
Try It Yourself
Build a small product catalogue using a simple repository pattern. Practice namespaces, constructor promotion, and the kind of class organisation Composer's autoloader makes effortless.
<?php
namespace App\Catalogue {
class Product
{
public function __construct(
public readonly string $sku,
public readonly string $name,
public readonly float $price,
public readonly string $category,
) {}
public function formattedPrice(): string
{
return '$' . number_format($this->price, 2);
}
}
class ProductCatalogue
{
/** @var Product[] */
private array $products = [];
public function add(Product $product): void
{
$this->products[$product->sku] = $product;
}
public function findBySku(string $sku): ?Product
{
return $this->products[$sku] ?? null;
}
public function filterByCategory(string $category): array
{
return array_values(
array_filter(
$this->products,
fn(Product $p) => $p->category === $category,
)
);
}
public function cheaperThan(float $maxPrice): array
{
return array_values(
array_filter(
$this->products,
fn(Product $p) => $p->price < $maxPrice,
)
);
}
public function count(): int
{
return count($this->products);
}
}
}
namespace {
use App\Catalogue\Product;
use App\Catalogue\ProductCatalogue;
$catalogue = new ProductCatalogue();
$catalogue->add(new Product('BOOK-001', 'Clean Code', 35.00, 'books'));
$catalogue->add(new Product('BOOK-002', 'The Pragmatic Programmer', 45.00, 'books'));
$catalogue->add(new Product('GEAR-001', 'Mechanical Keyboard', 120.00, 'gear'));
$catalogue->add(new Product('GEAR-002', 'USB-C Hub', 29.99, 'gear'));
$catalogue->add(new Product('GEAR-003', 'Monitor Stand', 49.99, 'gear'));
echo "Total products: " . $catalogue->count() . "\n\n";
echo "Books:\n";
foreach ($catalogue->filterByCategory('books') as $p) {
echo " [{$p->sku}] {$p->name} — {$p->formattedPrice()}\n";
}
echo "\nItems under \$50:\n";
foreach ($catalogue->cheaperThan(50.00) as $p) {
echo " [{$p->sku}] {$p->name} — {$p->formattedPrice()}\n";
}
echo "\nLookup GEAR-001: ";
$keyboard = $catalogue->findBySku('GEAR-001');
echo $keyboard?->name ?? 'not found';
echo "\n";
}
Key Takeaways
- Composer is PHP's standard dependency manager — it downloads packages, resolves version conflicts, and generates an autoloader from a single
composer.jsonfile. composer installinstalls exact versions recorded incomposer.lock, ensuring every developer and deployment environment runs identical code.vendor/autoload.phpis the single require you need — after including it, all installed packages and your own PSR-4 classes load automatically on first use.- Version constraints use semantic versioning operators; the caret (
^) is the safest default, allowing compatible updates without risking breaking changes. - PSR-4 autoloading maps namespace prefixes to directory paths, keeping class organisation predictable and eliminating manual
requirechains. composer dump-autoloadregenerates the autoloader after you change namespace mappings or add new source directories.- The
vendor/directory should always be excluded from version control via.gitignore— onlycomposer.jsonandcomposer.lockare committed.
Pro Tip: Run
composer outdatedregularly to see which installed packages have newer versions available. Pair it withcomposer update --dry-runto preview what would change before committing to an update — this habit catches breaking changes early and keeps your dependencies healthy without surprises.
Next Steps
You can now manage dependencies with Composer, configure PSR-4 autoloading, understand version constraints, and structure projects the modern PHP way. Your workflow now matches how professional teams work.
How do you know your code actually works? Manual testing is slow, error-prone, and does not scale. Testing with PHPUnit teaches you to write automated tests that verify your code behaves correctly, catch regressions before they reach production, and give you confidence to refactor. PHPUnit is installed via Composer (which you just learned), and testing is a skill that employers and open-source maintainers expect every serious PHP developer to have.