LearningPHP.org
LessonsPlaygroundAbout
Sign In
Lessons/basics/Composer And Packages
PreviousPracticeNext

TL;DR

Learn Composer, PHP's dependency manager. Install packages, manage versions, configure autoloading, and structure modern PHP projects.

Key concepts

  • PHP Composer
  • Composer tutorial
  • PHP packages
  • PHP dependency management
Loading...

Next lesson

Testing With Phpunit

Learn PHP testing with PHPUnit. Write test classes, use assertions, data providers, and mocking for reliable, maintainable code.

25 min

Related lessons

  • Introduction to PHPStart learning PHP from scratch. Understand why PHP powers millions of websites and write your first server-side script.
  • Variables and Data TypesLearn PHP variables and data types including strings, integers, floats, booleans, and type casting for type-safe PHP development.
  • FunctionsLearn PHP functions with typed parameters, return types, closures, and arrow functions. Build reusable code blocks for any project.

Also learn

SQLMaster SQL & DatabasesJavaScriptMaster JavaScript Programming

Also learn

SQLMaster SQL & DatabasesJavaScriptMaster JavaScript Programming

A14A

Building digital products that matter.

© 2026 A14A. All rights reserved.
KVK: 87105004PrivacyTerms

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.

ConstraintMeaning
^4.0>=4.0.0 and <5.0.0 (same major)
~4.2>=4.2.0 and <5.0.0
4.1.3Exactly 4.1.3
>=4.0 <5.0Explicit 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.json file.
  • composer install installs exact versions recorded in composer.lock, ensuring every developer and deployment environment runs identical code.
  • vendor/autoload.php is 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 require chains.
  • composer dump-autoload regenerates the autoloader after you change namespace mappings or add new source directories.
  • The vendor/ directory should always be excluded from version control via .gitignore — only composer.json and composer.lock are committed.

Pro Tip: Run composer outdated regularly to see which installed packages have newer versions available. Pair it with composer update --dry-run to 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.

Continue to Testing with PHPUnit -->