Skip to main content

πŸ“¦ Dependency Management with Composer

Nobody builds a modern PHP app alone β€” you stand on the shoulders of thousands of open-source libraries. Composer is the tool that fetches those libraries, keeps their versions sane, and wires them into your code automatically. Learn it well and you unlock the entire PHP ecosystem.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what problem Composer solves and how it works
  • Read and write a composer.json file with dependencies and metadata
  • Interpret semantic-version constraints like ^, ~, and *
  • Describe the role of composer.lock and why it's committed to Git
  • Configure and use PSR-4 autoloading for your own classes
  • Automate tasks with Composer scripts

Estimated Time: 40–50 minutes  β€’  Difficulty: Beginner–Intermediate

Hands-on: Build a small project that installs and uses a real package.

In This Lesson

Why Composer Exists

Before Composer (which arrived in 2012), adding a library to a PHP project meant downloading a zip, extracting it somewhere, and hand-writing require statements β€” then repeating that for every library that library depended on. Get one version wrong and things broke in mysterious ways. This tangle has a name: dependency hell.

Composer replaces all of that. You declare what you need in one file, run one command, and Composer resolves the entire dependency graph, downloads the right versions, and generates an autoloader so you never write a require for a library again.

How Composer resolves dependencies Your composer.json is read by Composer, which fetches packages from the Packagist repository into a vendor directory and generates an autoloader your project uses. composer.json you declare needs Composer Packagist package registry vendor/ downloaded code autoload.php your app uses it
Figure 1 β€” You declare dependencies in composer.json; Composer downloads them from Packagist into vendor/ and generates an autoloader your code simply includes.

πŸ“– Key Terms

Package: a reusable library, identified as vendor/name (e.g. monolog/monolog).

Packagist: the central public registry Composer searches by default.

vendor/: the folder where Composer places all downloaded packages. You never edit it or commit it.

Installing Composer

Composer is a single executable. On Windows, the easiest route is the official installer, which also wires up your PATH:

# Windows: download and run Composer-Setup.exe from getcomposer.org

On macOS and Linux, download and verify the installer, then move it somewhere on your PATH so composer works from any folder:

# Download the installer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

# Run it, then make it globally available
php composer-setup.php
sudo mv composer.phar /usr/local/bin/composer

# Clean up and verify
php -r "unlink('composer-setup.php');"
composer --version    # expect Composer 2.x

πŸ’‘ Keep Composer current

Composer updates itself in place: composer self-update. Composer 2 is dramatically faster than the old 1.x line at resolving dependencies β€” make sure you're on 2.x.

Anatomy of composer.json

The composer.json file is the heart of every Composer project. You can generate one interactively with composer init, or write it by hand. Here's a well-formed example, annotated:

{
    "name": "raydelapaz/demo-app",
    "description": "A small demo project",
    "type": "project",
    "license": "MIT",
    "require": {
        "php": "^8.3",
        "monolog/monolog": "^3.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    },
    "config": {
        "sort-packages": true
    }
}
KeyPurpose
namePackage identifier in vendor/name form
requireRuntime dependencies the app needs to function
require-devTools only needed while developing (tests, linters) β€” skipped in production
autoloadHow your own classes map to files (see the PSR-4 section)
autoload-devAutoloading for test-only code
configComposer behaviour tweaks (e.g. keeping packages sorted)

The commands you'll use daily

# Add a runtime dependency (edits composer.json + installs it)
composer require monolog/monolog

# Add a dev-only dependency
composer require --dev phpunit/phpunit

# Install everything listed (uses composer.lock if present)
composer install

# Update packages to newer allowed versions (and rewrite the lock)
composer update

# Remove a package
composer remove monolog/monolog

# Check for known security advisories in your dependencies
composer audit

⚠️ install vs. update β€” a costly mistake

composer install respects the exact versions in composer.lock. composer update pulls newer versions and rewrites the lock. Running update on a production deploy β€” when you only meant to install β€” is a classic way to accidentally ship untested library versions.

Version Constraints

Composer follows Semantic Versioning: a version like 3.5.2 is MAJOR.MINOR.PATCH. Major bumps may break your code; minor bumps add features compatibly; patch bumps fix bugs. Constraints tell Composer which versions are acceptable.

ConstraintAllowsBlocks
^3.5>= 3.5.0 and < 4.0.0 (minor + patch updates)4.0.0 and up
~3.5>= 3.5.0 and < 4.0.04.0.0 and up
~3.5.2>= 3.5.2 and < 3.6.0 (patch only)3.6.0 and up
3.5.*any 3.5.x patch3.6.0 and up
>=3.53.5 or anything newer, incl. 4.x, 5.xnothing above 3.5
3.5.2exactly 3.5.2everything else
Version constraint ranges on a number line A version line from 3.5 to 5.0 showing that caret 3.5 covers 3.x up to but not including 4.0, tilde 3.5.2 covers only 3.5 patches, and greater-than-or-equal 3.5 covers everything from 3.5 onward. 3.5 3.9 4.0 5.0 ^3.5 (3.5 up to < 4.0) ~3.5.2 (3.5 patches only) >=3.5 (3.5 and everything after)
Figure 2 β€” The caret (^) is the everyday default: it takes safe minor and patch updates but stops before the next major version that could break you.

βœ… Rule of thumb

Use ^ for almost everything. It honours SemVer: you get bug fixes and new features automatically, but Composer won't jump you to a major version that might break your code.

composer.json vs. composer.lock

These two files work as a pair, and understanding the difference is what separates confident developers from confused ones.

composer.jsoncomposer.lock
Written byYou (by hand or composer require)Composer, automatically
ContainsVersion ranges you'll acceptThe exact versions actually installed
Example entry"monolog/monolog": "^3.0""version": "3.7.0" + a content hash
Commit to Git?YesYes (for applications)

Here's why the lock file matters: your composer.json might say ^3.0, which could mean 3.0.0 today and 3.9.5 next month. Without a lock file, your laptop, a teammate's machine, and the production server could each install different versions β€” the definition of "works on my machine." The lock file pins the exact version everyone gets.

flowchart TD A[composer.json
^3.0 range] -->|composer update| B[composer.lock
pins 3.7.0] B -->|composer install| C[Your laptop: 3.7.0] B -->|composer install| D[Teammate: 3.7.0] B -->|composer install| E[Production: 3.7.0]

⚠️ Never gitignore composer.lock (for apps)

For an application, commit composer.lock so every environment installs identical versions. (Reusable libraries traditionally omit it, since the app that consumes them decides final versions.) Always gitignore the vendor/ folder either way.

Autoloading with PSR-4

Composer's autoloader is arguably its best feature. Instead of manually require-ing every file, you include one line and PHP loads classes on demand β€” for both third-party packages and your own code.

<?php
// One line, at the top of your entry point:
require __DIR__ . '/vendor/autoload.php';

// Now any installed class just works, no manual require:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/app.log'));
$log->info('It works!');

Autoloading your own classes (PSR-4)

PSR-4 maps a namespace prefix to a directory. With this in composer.json…

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

…the namespace App\ points at the src/ folder. So the class App\Services\Mailer must live in src/Services/Mailer.php. The mapping is purely by convention β€” get the folder and file names right and it just works:

<?php
// File: src/Services/Mailer.php
declare(strict_types=1);

namespace App\Services;

class Mailer
{
    public function send(string $to, string $subject): bool
    {
        // ... real sending logic ...
        return true;
    }
}
<?php
// File: index.php
require __DIR__ . '/vendor/autoload.php';

use App\Services\Mailer;

$mailer = new Mailer();          // autoloaded from src/Services/Mailer.php
$mailer->send('ada@example.com', 'Hi');

πŸ’‘ When to run dump-autoload

After you add a new namespace mapping or create classes that a classmap needs to see, regenerate the autoloader with composer dump-autoload. For production, add --optimize (or -o) to build a fast static classmap: composer dump-autoload -o.

Composer Scripts

Composer can run named shortcuts for common tasks β€” testing, linting, starting a dev server β€” so your whole team invokes them the same way. Define them under scripts:

{
    "scripts": {
        "start": "php -S localhost:8080 -t public/",
        "test": "phpunit --colors=always",
        "lint": "phpcs --standard=PSR12 src/",
        "check": [
            "@lint",
            "@test"
        ]
    }
}
# Run any script by name:
composer start        # launches the dev server
composer test         # runs the test suite
composer check        # runs lint, then test (a script that calls scripts)

# Pass extra arguments after --
composer test -- --filter MailerTest

Composer also fires scripts on lifecycle events. For example, post-install-cmd runs after composer install β€” handy for copying a default .env file so a fresh checkout is ready to go:

{
    "scripts": {
        "post-install-cmd": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ]
    }
}

βœ… Why this matters

Scripts turn tribal knowledge ("run this exact phpunit incantation") into a documented, one-word command. New contributors get productive faster, and CI pipelines call the same composer check you do locally.

Hands-on Exercise

πŸ‹οΈ Build a project that uses a real package

Objective: Initialize a Composer project, install a logging library, autoload your own class, and produce output β€” the full loop.

Instructions:

  1. Create a folder and run composer init (accept the defaults; skip adding dependencies interactively).
  2. Install Monolog: composer require monolog/monolog. Observe the new vendor/ folder and composer.lock.
  3. Add a PSR-4 mapping "App\\": "src/" to composer.json, then run composer dump-autoload.
  4. Create src/Activity.php with a class App\Activity that has a method record(string $message) which logs the message to a file using Monolog.
  5. Create index.php that includes the autoloader, instantiates Activity, records two messages, then confirm they appear in the log file.
πŸ’‘ Hint

Your class file must match the namespace exactly: App\Activity lives in src/Activity.php and starts with namespace App;. Inside the class, create a Monolog\Logger and push a Monolog\Handler\StreamHandler pointed at a log path. Don't forget require __DIR__ . '/vendor/autoload.php'; at the top of index.php.

βœ… Sample solution
<?php
// File: src/Activity.php
declare(strict_types=1);

namespace App;

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Level;

class Activity
{
    private Logger $log;

    public function __construct(string $file = 'activity.log')
    {
        $this->log = new Logger('activity');
        $this->log->pushHandler(new StreamHandler($file, Level::Info));
    }

    public function record(string $message): void
    {
        $this->log->info($message);
    }
}
<?php
// File: index.php
require __DIR__ . '/vendor/autoload.php';

use App\Activity;

$activity = new Activity(__DIR__ . '/activity.log');
$activity->record('User signed in');
$activity->record('User viewed dashboard');

echo "Logged! Check activity.log\n";

Run it with php index.php. Open activity.log and you'll see two timestamped activity.INFO lines. You installed a package, autoloaded your own namespaced class, and used both together β€” the everyday Composer workflow.

🎯 Quick Quiz

Question 1: What does the constraint ^3.5 allow?

Question 2: Which file should you commit to Git for an application, and which should you ignore?

Question 3: With "App\\": "src/" as your PSR-4 mapping, where must the class App\Http\Router live?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Composer manages PHP dependencies β€” you declare needs, it resolves and downloads them from Packagist into vendor/.
  • composer.json holds version ranges; composer.lock pins the exact versions everyone installs.
  • Use ^ for most constraints β€” safe updates without surprise major-version breaks.
  • composer install respects the lock; composer update changes it. Know the difference.
  • PSR-4 autoloading maps a namespace to a directory so classes load on demand β€” include vendor/autoload.php once.
  • Composer scripts turn common tasks into one-word commands shared by your whole team.

πŸ“š Further Reading

πŸš€ What's Next?

You now have the full backend toolkit: the PHP language, a working environment, and Composer for pulling it all together. Next you'll put every piece to work in the Weekend Project β€” a hands-on build that ties the module's backend fundamentals into one small application.

πŸŽ‰ Module toolkit complete!

Language, environment, dependencies β€” all set. Let's build something real this weekend.