Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 006 / 100

Composer and autoloading: a hands-on guide for engineers

Dependency management and PSR-4 autoloading.

Expona Admin7 min read

Getting started with dependency management

When you're working on production systems handling real customer transactions, you quickly realize that reinventing the wheel for every feature is neither practical nor safe. That's where Composer and autoloading come in. Composer and autoloading: a hands-on guide for engineers means understanding how to pull in vetted packages, wire them into your codebase efficiently, and keep your team on the same version of every dependency. This guide walks through the mechanics that power modern PHP applications, from the initial composer require command through vulnerability audits before deployment.

Whether you're joining a team that's already using Composer or starting a new project from scratch, these patterns will feel immediately familiar once you see them in action. We've all hit the scenario where a junior developer somehow ends up with a different version of a package than what's in production, causing hours of debugging. The practices we cover here prevent exactly that kind of friction.

Composer require: adding packages to your project

Every project starts with dependencies. When you need to work with HTTP requests, database queries, or authentication, you don't write those libraries yourself—you declare them in Composer and let it handle the rest.

To add a package to your project, you run:

php
composer require monolog/monolog

That single command does several things at once. Composer downloads the package and its own dependencies from Packagist (the default repository), resolves version conflicts, and updates your composer.json and composer.lock files. Your composer.json now shows that you depend on Monolog, and composer.lock records the exact version that was installed—critical for reproducibility.

Let's see this in a real scenario. You're building a logging system for a ride-hailing platform. Your composer.json might look like this after a few require commands:

php
{
    "name": "acme/ride-hailing",
    "require": {
        "php": "^8.1",
        "monolog/monolog": "^3.0",
        "symfony/http-client": "^6.0",
        "doctrine/orm": "^2.14"
    }
}

The caret (^) prefix tells Composer you're okay with minor and patch updates, but not major breaking changes. If you need a specific version or want to lock to an exact release, you use different constraints—~ for patch-only updates, or just 1.2.3 for exact versions.

For development-only packages like testing frameworks, you use --dev:

php
composer require --dev phpunit/phpunit

This keeps your production environment lean. When you deploy, you run composer install --no-dev to skip test dependencies, reducing image size and attack surface.

PSR-4 autoloading: mapping namespaces to folders

Once packages are installed, they need to be loaded into memory when your code runs. PSR-4 is the PHP Standard Recommendation that defines how namespaces map to directory structures. Composer generates and maintains an autoloader that implements PSR-4, eliminating the need for manual require or include statements.

Here's how it works. When you define PSR-4 mappings in your composer.json:

php
{
    "autoload": {
        "psr-4": {
            "RideHailing\\": "src/"
        }
    }
}

You're telling Composer: "Any class in the namespace RideHailing should be found in the src/ directory." If you have a class RideHailing\Drivers\Driver, Composer will look for it at src/Drivers/Driver.php. The namespace hierarchy maps directly to the folder structure.

Let's make this concrete. Your project structure looks like this:

php
ride-hailing/
├── composer.json
├── src/
│   ├── Drivers/
│   │   ├── Driver.php
│   │   └── DriverRepository.php
│   ├── Rides/
│   │   └── Ride.php
│   └── Notifications/
│       └── NotificationService.php
└── vendor/

Your Driver.php file starts with:

php
namespace RideHailing\Drivers;

class Driver {
    public function __construct(private string $id) {}
}

And in another file, you can use it immediately without any require:

php
namespace RideHailing\Rides;

use RideHailing\Drivers\Driver;

class Ride {
    public function __construct(private Driver $driver) {}
}

Composer generates an autoloader file at vendor/autoload.php. Your entry point (usually public/index.php in a web app) includes just one line:

php
require_once __DIR__ . '/../vendor/autoload.php';

That single include wires up all PSR-4 mappings for both your code and every third-party package. The autoloader uses a class map for installed packages (generated once during installation) and lazy-loads your own application code on demand. This is fast in production and flexible during development.

You can define multiple PSR-4 namespaces if your project is organized into distinct domains:

php
{
    "autoload": {
        "psr-4": {
            "RideHailing\\Drivers\\": "src/domain/drivers/",
            "RideHailing\\Rides\\": "src/domain/rides/",
            "RideHailing\\Payments\\": "src/domain/payments/"
        }
    }
}

After modifying composer.json, run composer dump-autoload to regenerate the autoloader without fetching packages again.

Committing composer.lock for reproducible deployments

Here's a critical practice that separates teams that deploy reliably from those that don't: always commit your composer.lock file to version control.

When you run composer install, Composer first checks if composer.lock exists. If it does, it installs the exact versions listed in the lock file, ignoring the version constraints in composer.json. This means your local environment, your colleague's machine, your CI/CD pipeline, and your production servers all run identical versions of every dependency.

Without composer.lock committed, each environment runs composer install and picks up whatever the latest version satisfying your constraints happens to be at that moment. If a package author releases a minor version bump that inadvertently breaks something, your production environment might get it while your development environment doesn't. Debugging that kind of environment mismatch is nightmarish.

Your git workflow looks like this:

  1. Developer adds a new package: composer require some/package
  2. Both composer.json and composer.lock are modified
  3. Both files are committed together to version control
  4. Other team members pull and run composer install
  5. They get the exact same package versions

If you need to update dependencies—say, to get a security patch—you run composer update, which updates composer.lock` to newer versions that still satisfy your constraints. That updated lock file then gets committed, and everyone's environment updates together.

In a production deployment, you typically run:

php
composer install --no-dev --prefer-dist

The --no-dev flag skips development dependencies. The --prefer-dist flag downloads pre-built archives instead of cloning git repositories, which is significantly faster and reduces disk I/O during deployment.

composer audit: checking for known vulnerabilities

Dependencies aren't just about convenience—they're also a vector for security issues. A vulnerable package in your dependency tree can expose your entire application. Composer provides composer audit to scan your installed packages against a database of known vulnerabilities.

Running it is straightforward:

php
composer audit

The output tells you which packages have vulnerabilities, their severity level, and which versions fix the issue. For example:

php
symfony/http-kernel 6.0.0 has 2 vulnerability/vulnerabilities.
├─ Remote Code Execution (RCE) via deserialization
└─ Information Disclosure via error handler

symfony/http-kernel 6.0.1 resolves the issues.

You'd then update that package and re-run composer audit` to confirm the vulnerability is gone:

php
composer require symfony/http-kernel:^6.0.1

In CI/CD pipelines, add composer audit --exit-code as a build step. The exit code 1 signals failure if vulnerabilities are found, preventing merged PRs or deployments that introduce known security issues. This is as critical as running your test suite.

Best practice: run audits regularly, not just when adding new packages. New vulnerabilities are discovered all the time. Many teams run composer audit daily or weekly and get alerted if issues are found.

Common mistakes

  • Not committing composer.lock: This creates environment drift. Every engineer ends up with different versions, causing "works on my machine" bugs.
  • Using require without specifying constraints: composer require symfony/console without a version constraint can pull major versions that introduce breaking changes. Be explicit with constraints like ^6.0.
  • Running composer install on production without the lock file: If your lock file isn't in version control, production can install different versions than your staging environment tested.
  • Ignoring composer audit warnings: Postponing security updates is a technical debt that compounds. Address vulnerabilities as soon as patches are available.
  • Keeping development dependencies in production: Forgetting --no-dev` during production deployment increases image size and attack surface. Test frameworks don't belong in production.
  • Modifying vendor/ manually: The vendor directory is generated by Composer. Any manual changes get wiped on the next composer install. Always update through Composer.

Key takeaways

  • composer require fetches packages and their dependencies from Packagist, updating both composer.json and composer.lock.
  • PSR-4 autoloading maps namespaces to directories, eliminating manual require statements and enabling clean class loading.
  • Always commit composer.lock to version control to guarantee that every environment runs identical dependency versions.
  • Run composer audit regularly in CI/CD pipelines to catch vulnerabilities before they reach production.
  • Use version constraints like ^ thoughtfully; they balance flexibility with stability.
  • Deploy with --no-dev and --prefer-dist for faster, leaner production environments.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles