Composer Packages

Reviewed & published by Brayan K

By the end of this lesson you'll manage third-party libraries with Composer, wire up PSR-4 autoloading so your classes load themselves, choose the right version constraints, and publish your own package to Packagist for the world to use.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ What Composer Is & the Four Commands

Composer is PHP's dependency manager — a tool that downloads the third-party libraries your project needs and keeps track of their versions. You almost never write a logging system, a date library, or an HTTP client yourself; you require a battle-tested one. There are four commands you'll use daily: composer init creates a new composer.json, composer require adds a package, composer install installs the exact versions a project already locked, and composer update upgrades to the newest versions your rules allow.

# Composer is PHP's dependency manager. It downloads the libraries
# your project needs, and writes a tiny PHP file that loads them all.

# 1) Start a brand-new project. This asks a few questions, then
#    writes a composer.json describing your project.
composer init

# 2) Add a dependency. This downloads the package into vendor/,
#    records it in composer.json, and pins the exact version in composer.lock.
composer require monolog/monolog

# 3) On a fresh checkout (e.g. a teammate clones the repo),
#    install grabs EXACTLY the versions listed in composer.lock.
composer install

# 4) update re-resolves to the newest versions your constraints allow
#    and REWRITES composer.lock with what it picked.
composer update

2️⃣ composer.json vs composer.lock

These two files trip up everyone at first, so make the distinction stick. composer.json is your wish list: it says which packages you want and the version ranges you'll accept. composer.lock is the receipt: Composer generates it automatically, recording the single exact version of every package — and every sub-dependency — it actually installed. Because the lock pins everything precisely, composer install reproduces the identical dependency tree on every machine.

// composer.json  —  what you WANT (a wish list with version ranges)
{
    "name": "acme/blog",
    "type": "project",
    "require": {
        "php": ">=8.1",
        "monolog/monolog": "^3.7"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    }
}

// composer.lock  —  what you GOT (a receipt with EXACT versions)
//   Generated automatically. You never edit it by hand.
//   It records the precise version of every package AND every
//   sub-dependency, so every machine installs the identical tree.
{
    "packages": [
        { "name": "monolog/monolog", "version": "3.7.0" },
        { "name": "psr/log",         "version": "3.0.2" }
    ]
}

3️⃣ PSR-4 Autoloading & vendor/autoload.php

In the old days you wrote a require for every single class file — tedious and fragile. PSR-4 is the modern standard that ends that: it maps a namespace prefix to a folder, so Composer can find a class file from its name alone. You declare the mapping once in composer.json, run composer dump-autoload, then require 'vendor/autoload.php' a single time. From then on, the first time you use a class, Composer loads the matching file for you. A sub-namespace simply means a sub-folder.

<?php
// PSR-4 is the standard that maps a NAMESPACE to a FOLDER, so Composer
// can find a class file from its name alone — no require statements.
//
// In composer.json you declare the mapping:
//   "autoload": { "psr-4": { "Acme\\Blog\\": "src/" } }
//
// That single line means:
//   prefix  Acme\Blog\   ->  the  src/  directory
//
//   Acme\Blog\Post            ->  src/Post.php
//   Acme\Blog\Model\Comment  ->  src/Model/Comment.php   (sub-namespace = sub-folder)

// --- src/Post.php ---
namespace Acme\Blog;          // namespace MUST match the folder under the prefix

class Post {
    public function __construct(public string $title) {}
    public function slug(): string {
        return strtolower(str_replace(' ', '-', $this->title));
    }
}

// --- index.php (your app entry point) ---
require 'vendor/autoload.php';   // ONE require — Composer wires up every class

use Acme\Blog\Post;            // pull the class in by its full name

$post = new Post('Hello Composer World');
echo $post->slug() . "\n";      // Composer auto-loads src/Post.php on first use

The magic is vendor/autoload.php — a file Composer writes for you. Including it registers an autoloader: a function PHP calls whenever you reference a class it hasn't loaded yet. It translates Acme\Blog\Post into src/Post.php and loads it on demand. If you add a new class and PHP says it can't find it, you usually just need to run composer dump-autoload to refresh the map.

4️⃣ Semantic Versioning & Constraints (^ vs ~)

Every package version is MAJOR.MINOR.PATCH — a patch is a bug fix, a minor adds features without breaking anything, and a major bump signals a breaking change. In composer.json you don't pin one fixed version; you give a constraint so you keep receiving safe fixes. The caret ^ is the common default — it allows everything up to the next major. The tilde ~ is stricter, usually allowing patch releases only.

<?php
// Semantic Versioning (SemVer) is MAJOR.MINOR.PATCH, e.g. 3.7.2
//   PATCH (3.7.2 -> 3.7.3)  bug fix,    backward compatible
//   MINOR (3.7.0 -> 3.8.0)  new feature, backward compatible
//   MAJOR (3.x   -> 4.0.0)  BREAKING change, may need code changes
//
// In composer.json you don't pin one version — you give a RANGE
// so you keep getting safe fixes without surprise breakages.

$constraints = [
    // ^ (caret): allow everything up to the next MAJOR — the common default
    '^3.7.0'  => '>=3.7.0  and  <4.0.0',   // gets 3.8, 3.9, 3.10 ... not 4.0
    // ~ (tilde): allow up to the next MINOR — more cautious
    '~3.7.0'  => '>=3.7.0  and  <3.8.0',   // patches only: 3.7.1, 3.7.2 ...
    '~3.7'    => '>=3.7.0  and  <4.0.0',   // (two parts) up to next major
    '3.7.*'   => '>=3.7.0  and  <3.8.0',   // wildcard, same as ~3.7.0
];

foreach ($constraints as $written => $means) {
    echo str_pad($written, 9) . " means  " . $means . "\n";
}

5️⃣ Dev Dependencies & Scripts

Not every dependency ships to production. Tools like PHPUnit (testing) and PHPStan (static analysis) belong under require-dev — they're installed on your laptop and CI server but skipped on a production deploy with composer install --no-dev. You can also define scripts: named shortcuts for commands you run constantly, so a long incantation becomes a short composer test.

// composer.json can define SCRIPTS: named shortcuts for commands you
// run all the time. They live under the "scripts" key.
{
    "require-dev": {
        "phpunit/phpunit": "^11.0",
        "phpstan/phpstan": "^1.12"
    },
    "scripts": {
        "test":  "phpunit",
        "stan":  "phpstan analyse src --level=6",
        "check": [ "@stan", "@test" ]
    }
}

// Run them from the terminal — Composer finds the tool inside vendor/bin:
//   composer test     ->  runs phpunit
//   composer stan     ->  runs phpstan
//   composer check    ->  runs BOTH (the @-prefix calls another script)

6️⃣ Your Turn: Install & Autoload

Now you try. The terminal session below is almost complete — fill in each ___ using the 👉 hint, then check it against the Output panel.

# 🎯 YOUR TURN — fill in each blank marked ___ , then run it.
# You're starting a project and want the Guzzle HTTP client library.

# 1) The Composer subcommand that ADDS a dependency
composer ___ guzzlehttp/guzzle        # 👉 the verb that means "I require this"

# 2) After cloning a repo, the subcommand that installs the
#    EXACT versions already pinned in composer.lock
composer ___                          # 👉 not "update" — the one that respects the lock

# ✅ Expected: line 1 downloads guzzlehttp/guzzle into vendor/ ;
#    line 2 installs the locked versions for everyone on the team.

One more — this time PSR-4. The class is at src/Money/Price.php and the prefix Shop\ maps to src/. Give the class the matching namespace and load the autoloader.

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Composer

Command / TermExampleWhat It Does
composer initcomposer initCreate a new composer.json
composer requirecomposer require monolog/monologAdd & install a dependency
composer installcomposer installInstall exact versions from composer.lock
composer updatecomposer updateUpgrade within constraints, rewrite lock
composer dump-autoloadcomposer dump-autoloadRegenerate the PSR-4 autoload map
^1.2"monolog/monolog": "^3.7">=3.7.0 and <4.0.0 (up to next major)
~1.2"psr/log": "~3.0.0">=3.0.0 and <3.1.0 (patches only)
PSR-4"Acme\\": "src/"Map a namespace prefix to a folder

Mini-Challenge: Build a Publishable Library

No code is filled in this time — just a brief and an outline. Write it yourself, run the PHP part on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This mirrors how you'd actually scaffold a real package.

<?php
// 🎯 MINI-CHALLENGE: publish-ready composer.json + a PSR-4 class.
// No code is filled in — work from the steps, then run the PHP part.
//
// PART A — write a composer.json (as a PHP array, json_encode it) for a
//          library called "acme/slugger" that:
//   1. has "name", "description" and "license": "MIT"
//   2. requires php ">=8.1"
//   3. has a require-dev on "phpunit/phpunit": "^11.0"
//   4. autoloads PSR-4:  "Acme\\Slugger\\"  ->  "src/"
//
// PART B — write the class  Acme\Slugger\Slugger  with a method
//          slugify(string $text): string  that lowercases the text and
//          replaces spaces with hyphens. Then echo slugify('Hello World').
//
// PART C — list, in comments, the terminal steps to publish it:
//   git tag v1.0.0  ->  push tags  ->  submit the repo URL at packagist.org
//
// ✅ Expected output of Part B:  hello-world

// your code here

🎉 Lesson Complete!

Practice quiz

Which Composer command adds a new dependency to your project?

  • composer install
  • composer update
  • composer require
  • composer dump-autoload

Answer: composer require. composer require downloads the package into vendor/, records it in composer.json, and pins the version in composer.lock.

What does composer.json hold?

  • Your wish list: the packages you want and the version ranges you'll accept
  • The exact pinned version of every package
  • The downloaded library code
  • Your database credentials

Answer: Your wish list: the packages you want and the version ranges you'll accept. composer.json is the wish list of ranges like ^3.7; composer.lock is the receipt of exact installed versions.

Which command installs the EXACT versions already pinned in composer.lock?

  • composer require
  • composer update
  • composer init
  • composer install

Answer: composer install. composer install reads the lock file so every machine installs identical versions. composer update re-resolves ranges and rewrites the lock.

Should you commit composer.lock for a deployed application?

  • No — let each machine resolve its own
  • Yes — so everyone installs byte-for-byte identical dependencies
  • Only on Fridays
  • Only if it's under 1 MB

Answer: Yes — so everyone installs byte-for-byte identical dependencies. Commit composer.lock for applications (reproducible builds). For a reusable library you publish, don't commit it. Never commit vendor/.

What does PSR-4 autoloading map?

  • A namespace prefix to a base folder
  • A function name to a file line
  • A package to a Packagist URL
  • A class to a database table

Answer: A namespace prefix to a base folder. PSR-4 maps a namespace prefix to a folder, e.g. Acme\Blog\ -> src/, so Acme\Blog\Post is found at src/Post.php.

Which single line do you include to load every Composer-managed class?

  • require 'composer.json';
  • use Composer\Autoloader;
  • require 'vendor/autoload.php';
  • include 'vendor/all.php';

Answer: require 'vendor/autoload.php';. Including vendor/autoload.php registers the autoloader; classes are then loaded on demand the first time you use them.

What does the caret constraint ^3.7.0 allow?

  • Exactly 3.7.0 only
  • >=3.7.0 and <4.0.0 (up to but not including the next major)
  • >=3.7.0 and <3.8.0 (patches only)
  • Any version at all

Answer: >=3.7.0 and <4.0.0 (up to but not including the next major). The caret ^ allows everything up to the next major — so 3.8, 3.9, 3.10 but never 4.0. It's the common default.

What does the tilde constraint ~3.7.0 allow?

  • >=3.7.0 and <4.0.0
  • Exactly 3.7.0
  • Any 3.x or 4.x version
  • >=3.7.0 and <3.8.0 (patch releases only)

Answer: >=3.7.0 and <3.8.0 (patch releases only). ~3.7.0 (three parts) allows patches only: 3.7.1, 3.7.2 ... It's stricter than the caret.

Where do development-only tools like PHPUnit belong?

  • Under require
  • Under require-dev
  • In composer.lock manually
  • In the vendor/ folder you commit

Answer: Under require-dev. require-dev holds test/lint tools; they're skipped in production with composer install --no-dev.

How do you publish your own package so others can require it?

  • Email the zip to Composer
  • Run composer publish
  • Tag a git version (e.g. v1.0.0), push tags, and submit the repo URL to Packagist
  • Commit vendor/ to GitHub

Answer: Tag a git version (e.g. v1.0.0), push tags, and submit the repo URL to Packagist. Composer reads git tags as versions; you submit the repo URL at packagist.org and enable the webhook so new tags publish automatically.

Continue this course

Frequently asked questions

What is the difference between composer.json and composer.lock?

composer.json is your wish list: it states which packages you want and the version ranges you'll accept, like "^3.7". composer.lock is the receipt: after Composer resolves those ranges it records the single exact version of every package and every sub-dependency it installed. composer install reads the lock file so every machine ends up with the identical set of versions, while composer update ignores the lock, re-resolves your ranges to the newest allowed, and rewrites the lock with the result.

Should I commit composer.lock to git?

For an application (a website or service you deploy), yes — always commit composer.lock so every developer and your production server install byte-for-byte identical dependencies, giving you reproducible builds. For a reusable library you publish to Packagist, do not commit it: a library should be tested against the full range of versions its consumers might have, so you let each project resolve its own lock. Either way, never commit the vendor/ directory itself.

How does PSR-4 autoloading actually find my classes?

PSR-4 is a naming convention that maps a namespace prefix to a base folder. In composer.json you write something like "autoload": { "psr-4": { "Acme\\Blog\\": "src/" } }. After running composer dump-autoload, the namespace Acme\Blog\Post is expected at src/Post.php, and Acme\Blog\Model\Comment at src/Model/Comment.php — each sub-namespace is a sub-folder. You then require 'vendor/autoload.php' once, and Composer loads each class file automatically the first time you use it.

What is the difference between the ^ and ~ version constraints?

The caret ^ is the relaxed default: ^3.7.0 allows anything up to the next major version (>=3.7.0 and <4.0.0), so you receive new features and bug fixes that promise to be backward compatible. The tilde ~ is more cautious: ~3.7.0 allows patch releases only (>=3.7.0 and <3.8.0), while ~3.7 (two parts) allows up to the next major. Use ^ for most libraries and ~ when you want to lock a minor version and accept only bug-fix patches.

How do I publish my own package to Packagist?

Push your library to a public git repository (GitHub, GitLab), make sure it has a valid composer.json with a vendor/package name and a PSR-4 autoload block, then tag a release: git tag v1.0.0 and git push --tags — Composer reads git tags as version numbers. Finally, sign in at packagist.org, submit your repository URL, and enable the GitHub webhook so new tags publish automatically. After that, anyone can run composer require your-vendor/your-package.

Related lessons