Skip to main content
    Courses/PHP/Composer Packages

    Lesson 46 • Advanced

    Composer Packages 📦

    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.

    What You'll Learn in This Lesson

    • Install and update dependencies with require, install and update
    • Tell composer.json apart from composer.lock — and know which to commit
    • Map namespaces to folders with PSR-4 and load them via vendor/autoload.php
    • Read version constraints: ^ vs ~ and what each one allows
    • Separate runtime dependencies from dev-only tools, and add Composer scripts
    • Publish your own library to Packagist so others can require it

    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.

    The everyday Composer workflow
    # 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
    Output
    # composer require monolog/monolog
    Using version ^3.7 for monolog/monolog
    ./composer.json has been updated
    Running composer update monolog/monolog
    Loading composer repositories with package information
    Updating dependencies
    Lock file operations: 2 installs, 0 updates, 0 removals
      - Locking monolog/monolog (3.7.0)
      - Locking psr/log (3.0.2)
    Writing lock file
    Installing dependencies from lock file (including require-dev)
    Package operations: 2 installs, 0 updates, 0 removals
      - Installing psr/log (3.0.2)
      - Installing monolog/monolog (3.7.0)
    Generating autoload files
    Real terminal commands — run them in a project folder after installing Composer. The output shows what composer require monolog/monolog prints.

    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.

    The wish list and the receipt
    // 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" }
        ]
    }
    Run this in your own terminal or editor to see it work.

    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.

    PSR-4: one namespace map, zero manual requires
    <?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
    Output
    hello-composer-world
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    What each constraint actually allows
    <?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";
    }
    Output
    ^3.7.0    means  >=3.7.0  and  <4.0.0
    ~3.7.0    means  >=3.7.0  and  <3.8.0
    ~3.7      means  >=3.7.0  and  <4.0.0
    3.7.*     means  >=3.7.0  and  <3.8.0
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    require-dev tools and named scripts
    // 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)
    Run this in your own terminal or editor to see it work.

    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: the right Composer commands
    # 🎯 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.
    Output
    composer require guzzlehttp/guzzle
    composer install
    Fill the two ___ blanks with the correct Composer subcommands. One adds a package; the other installs the locked versions.

    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.

    🎯 Your turn: namespace + autoloader
    <?php
    // 🎯 YOUR TURN — a PSR-4 class needs the right namespace and the autoloader.
    // The class file is  src/Money/Price.php , and composer.json maps:
    //   "autoload": { "psr-4": { "Shop\\": "src/" } }
    
    // 1) Give the class the namespace that matches its folder under the prefix.
    //    File is src/Money/Price.php and the prefix Shop\ maps to src/ ...
    ___ Shop\Money;            // 👉 the keyword that declares a namespace + Shop\Money
    
    class Price {
        public function __construct(public int $cents) {}
        public function pounds(): string { return '£' . number_format($this->cents / 100, 2); }
    }
    
    // 2) In index.php, load every Composer class with ONE line.
    require '___';             // 👉 the file Composer generates: vendor/autoload.php
    
    use Shop\Money\Price;
    echo (new Price(2599))->pounds() . "\n";
    
    // ✅ Expected output:
    //    £25.99
    Output
    £25.99
    Replace the first ___ with the keyword that declares the namespace Shop\Money, and the second with the path to Composer's autoloader. Output should be £25.99.

    Common Errors (and the fix)

    • You committed the whole vendor/ folder to git — never do this. vendor/ is generated by composer install and can be hundreds of megabytes. Add a .gitignore line /vendor/ and commit only composer.json and composer.lock.
    • "Class not found" even though the file exists — your autoload map is stale. After adding a new class (or changing the PSR-4 mapping) run composer dump-autoload to regenerate vendor/autoload.php. Also check the namespace exactly matches the folder under the prefix.
    • Teammates get different versions / "it works on my machine" — you didn't commit composer.lock for your application. Without it, each person's composer install resolves the ranges differently. Commit the lock file so everyone installs identical versions.
    • A composer update broke your app — your constraint was too loose, like * or >=1.0, so it pulled in a breaking major release. Use a real range such as ^3.7, and update one package at a time with composer update vendor/package.

    Pro Tips

    • 💡 Default to ^ constraints. They give you bug fixes and features automatically while protecting you from breaking major releases.
    • 💡 Run composer outdated to see which dependencies have newer versions, and composer audit to flag known security vulnerabilities.
    • 💡 Deploy with composer install --no-dev --optimize-autoloader — it skips test tools and builds a faster class map for production.

    📋 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

    Frequently Asked Questions

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    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.

    🎯 Mini-Challenge: composer.json + PSR-4 class + publish steps
    <?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
    Build the composer.json, write the Slugger class, and list the publish steps. The slugify call should print hello-world.

    🎉 Lesson Complete!

    • Composer manages dependencies: init, require, install, update
    • composer.json is your wish list (ranges); composer.lock is the receipt (exact versions)
    • ✅ Commit composer.lock for apps, not for libraries — and never commit vendor/
    • PSR-4 maps a namespace to a folder; require 'vendor/autoload.php' loads classes on demand
    • ^ allows up to the next major; ~ is stricter (usually patches only)
    • require-dev holds test/lint tools; scripts add named shortcuts
    • ✅ Publish to Packagist by tagging a git version and submitting the repo URL
    • Next lesson: Deployment — ship your PHP app with Nginx, PHP-FPM, and Docker

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.