14+ years building on WordPress / Replies in under 5 hours
Development 1 min read · Updated July 2026

Understanding composer.json and Composer for Modern WordPress Plugin/Theme Dependencies

Photo of Ajay Khandal
Ajay Khandal
WordPress Developer
Understanding composer.json and Composer for Modern WordPress Plugin/Theme Dependencies
TL;DR

Composer manages PHP library dependencies in WordPress plugins and themes: declare packages in composer.json under require (runtime) and require-dev (test/tooling), run composer install to download them and generate a single autoloader, then include vendor/autoload.php in your plugin bootstrap. For publicly distributed plugins, prefix all third-party namespaces with PHP-Scoper or Mozart to prevent conflicts with other plugins that load the same library. Commit composer.lock always; commit vendor/ for distributable plugins, gitignore it for CI/CD deployments.

Before Composer, integrating a third-party PHP library into a WordPress plugin meant downloading a zip, dropping files into a lib/ folder, writing a stack of require_once statements, and praying the library didn’t conflict with one already loaded by another plugin. Updating it meant repeating the whole process by hand.

Composer is the PHP dependency manager that ended all of that. It resolves version constraints, downloads every transitive dependency, and generates a single autoloader that makes every class in your project — both your own and third-party — available without a single require. This guide covers the full workflow: setting up composer.json, PSR-4 autoloading, the install/update cycle, commit strategy for vendor/, dev-only dependencies, and the WordPress-specific concerns of namespace prefixing and WPackagist.

How Composer Works

Composer pulls packages from Packagist, the default PHP package registry. Each package is identified by a vendor/name string (e.g. guzzlehttp/guzzle) and a version constraint using semantic versioning: ^7.0 means ≥7.0 and ~7.4 means ≥7.4 and * means any version (avoid this in production).

When you run composer install, Composer resolves the full dependency graph — every library your library depends on, transitively — finds a compatible set of versions, downloads them into vendor/, and generates vendor/autoload.php. Including that one file in your plugin bootstrap is all you ever need to do to access every installed class.

composer.json: The Package Manifest

Place composer.json in the root of your plugin or theme directory — never in the WordPress root or wp-content/. Each plugin manages its own dependencies in isolation.

{
    "name": "mycompany/my-plugin",
    "description": "A WordPress plugin for custom integrations.",
    "type": "wordpress-plugin",
    "license": "GPL-2.0-or-later",
    "require": {
        "php": ">=8.1",
        "guzzlehttp/guzzle": "^7.0"
    },
    "require-dev": {
        "phpunit/phpunit": "^10.0",
        "squizlabs/php_codesniffer": "^3.7",
        "brain/monkey": "^2.6"
    },
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "MyPlugin\\Tests\\": "tests/"
        }
    },
    "config": {
        "optimize-autoloader": true,
        "allow-plugins": {
            "dealerdirect/phpcodesniffer-composer-installer": true
        }
    }
}

Key sections explained

require — runtime dependencies that must be present for the plugin to function. Version constraint ^7.0 accepts any 7.x release ≥7.0 but rejects 8.0+.

require-dev — dependencies only needed for development and testing. PHPUnit, PHP_CodeSniffer, and Mockery belong here. They are excluded when you run composer install --no-dev for production.

autoload — tells Composer how to find your own classes. The PSR-4 mapping "MyPlugin\\": "src/" means any class in the MyPlugin\ namespace maps to a file in the src/ directory. MyPlugin\Admin\Settingssrc/Admin/Settings.php.

autoload-dev — the same mapping, but for test classes that should not be loaded in production.

config.optimize-autoloader — builds a classmap instead of relying on the PSR-4 filesystem scan at runtime, which is faster in production.

PSR-4 Autoloading: Namespace → File Path

PSR-4 is the PHP standard for mapping namespaces to directory structures. The rule is simple: the namespace prefix maps to a base directory, and sub-namespaces map to subdirectories within it. With "MyPlugin\\": "src/" declared:

Class File path
MyPlugin\Plugin src/Plugin.php
MyPlugin\Admin\Settings src/Admin/Settings.php
MyPlugin\Http\ApiClient src/Http/ApiClient.php
MyPlugin\Database\Repository src/Database/Repository.php

Each file must declare namespace MyPlugin; (or the appropriate sub-namespace) and the class name must match the filename exactly, including capitalisation. This is what makes dependency injection and PHPUnit testing clean in WordPress plugins — classes are self-contained units with explicit dependencies, not a pile of files tied together by require chains.

The Install / Update / Dump Cycle

# First setup (or after pulling from git)
composer install

# Upgrade dependencies to latest compatible versions
composer update

# Production build — no dev tools
composer install --no-dev --optimize-autoloader

# Regenerate autoloader after adding a new class
composer dump-autoload --optimize

composer install reads composer.lock and installs the exact versions pinned there. If no lock file exists, it resolves from composer.json and creates one. This is what you run on every new developer machine or CI environment — it guarantees every team member has identical versions.

composer update re-resolves all constraints, downloads newer compatible versions, and rewrites composer.lock. Run this deliberately to pick up security patches or new features. Never run it on a production server.

composer install --no-dev skips require-dev packages. Use this in your CI/CD pipeline for the production artifact — it keeps PHPUnit and PHPCS out of the shipped code.

composer dump-autoload --optimize regenerates vendor/autoload.php without re-downloading packages. Run this after manually adding a new class file if you’re not running install again.

composer.lock: What to Commit

composer.lock records the exact resolved version of every dependency and sub-dependency. Committing it guarantees that composer install produces the same output for everyone on the team and in CI — no surprise version bumps.

For WordPress plugins distributed to end users (WordPress.org, direct download): commit the entire vendor/ directory alongside the lock file. End users run your plugin as-is and cannot run Composer themselves.

For plugins deployed via CI/CD (like a custom client site): commit composer.lock but gitignore vendor/. Your pipeline runs composer install --no-dev during the build step, which is faster and produces a clean artifact. Add this to .gitignore:

# .gitignore inside your plugin directory
vendor/

Loading the Autoloader in Your Plugin

In your main plugin file — the one with the WordPress plugin header comment — include the autoloader before any code that uses namespaced classes:

<?php
/**
 * Plugin Name: My Plugin
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
    require_once __DIR__ . '/vendor/autoload.php';
}

// Now all namespaced classes are available

The file_exists() guard prevents a fatal error in environments where Composer hasn’t been run yet (a fresh git clone before composer install, or a staging server mid-deploy). It also prevents a conflict if another plugin includes the same library — though proper namespace prefixing (covered next) is the real solution to that problem.

use MyPlugin\Plugin;
use GuzzleHttp\Client;

// Instantiate your plugin's main class
$plugin = new Plugin();
$plugin->boot();

// Use Guzzle directly
$client = new Client( [ 'base_uri' => 'https://api.example.com/' ] );

WordPress-Specific: Namespace Prefixing to Prevent Plugin Conflicts

This is the most important WordPress-specific Composer concern. If your plugin depends on guzzlehttp/guzzle ^7.0 and another installed plugin depends on guzzlehttp/guzzle ^6.0, PHP can only load one version of the GuzzleHttp namespace — whichever plugin loads first wins, and the other breaks.

The solution is to prefix all third-party namespaces with your own plugin namespace before shipping. Two tools do this automatically:

  • PHP-Scoper — scans your vendor/ directory and rewrites all namespace declarations and use statements, outputting a new directory (e.g. vendor-prefixed/) where GuzzleHttp becomes MyPlugin\Deps\GuzzleHttp. It’s thorough but has a steeper learning curve.
  • Mozart (coenjacobs/mozart) — a Composer plugin that runs automatically after composer install and rewrites namespaces in-place inside vendor/. Easier to set up, but PHP-Scoper handles edge cases better.
{
    "require-dev": {
        "coenjacobs/mozart": "^0.7"
    },
    "extra": {
        "mozart": {
            "dep_namespace": "MyPlugin\\Deps\\",
            "dep_directory": "/vendor-prefixed/",
            "classmap_directory": "/vendor-prefixed/classes/",
            "packages": [
                "guzzlehttp/guzzle",
                "guzzlehttp/promises"
            ]
        }
    },
    "scripts": {
        "post-install-cmd": ["@mozart compose"],
        "post-update-cmd": ["@mozart compose"]
    }
}

Namespace prefixing is non-negotiable for any plugin that will be installed alongside arbitrary third-party plugins — i.e., anything distributed publicly or installed on shared client sites.

WPackagist: Installing WordPress Plugins and Themes via Composer

WPackagist mirrors the entire WordPress.org plugin and theme directory as a Composer repository. Add it to your project to manage WordPress plugins and themes as Composer packages — useful for full-site project management with tools like Bedrock:

{
    "repositories": [
        {
            "type": "composer",
            "url": "https://wpackagist.org",
            "only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
        }
    ],
    "require": {
        "wpackagist-plugin/advanced-custom-fields": "^6.0",
        "wpackagist-theme/twentytwentyfour": "*"
    }
}

WPackagist is most useful in Bedrock-style WordPress setups where the entire site — WordPress core, plugins, themes — is managed as a Composer project. For standard single-plugin development, it’s rarely needed.

Checklist: Composer in a WordPress Plugin

  1. composer.json lives inside the plugin/theme directory, never in the WordPress root.
  2. Runtime dependencies go in require; testing and linting tools go in require-dev.
  3. PHP version constraint is explicit: "php": ">=8.1".
  4. Autoloader uses PSR-4 with a unique namespace prefix to avoid collisions.
  5. composer.lock is committed to version control.
  6. vendor/ is committed for distributable plugins; gitignored and rebuilt by CI for private deployments.
  7. Main plugin file includes the autoloader with a file_exists() guard.
  8. Production build uses composer install --no-dev --optimize-autoloader.
  9. Third-party library namespaces are prefixed (PHP-Scoper or Mozart) for publicly distributed plugins.
  10. composer update is run deliberately and tested — never run on a live server without reviewing the lock diff first.

For the next step after getting Composer wired up, see the guide on dependency injection in WordPress — the autoloader makes it trivial to inject classes as constructor arguments rather than pulling globals. And once DI is in place, unit testing with PHPUnit becomes significantly easier: require-dev pulls in the test runner and mocking libraries, and the PSR-4 structure means test files mirror the source tree exactly.

Need help setting up Composer in an existing plugin, implementing namespace prefixing, or migrating a legacy codebase to PSR-4? Get in touch — I do this regularly as part of plugin refactoring and code modernization projects.

Frequently asked questions

It depends on how the plugin is deployed. For plugins distributed to end users — WordPress.org, paid plugins, direct download — commit vendor/ alongside the code. Users install the plugin by uploading a zip; they cannot run Composer themselves, so the dependencies must be included. For plugins deployed via CI/CD to a specific server or hosting environment, gitignore vendor/ and run composer install --no-dev --optimize-autoloader as a build step. Always commit composer.lock in both cases — it ensures the build produces identical package versions across every environment.

composer install reads the existing composer.lock and downloads exactly the versions pinned there, with no version resolution. It is deterministic and safe to run in CI or on a fresh clone. composer update re-resolves all version constraints against composer.json, picks the newest compatible versions, and rewrites composer.lock. Run update deliberately to adopt security patches or new library features, then commit the updated lock file after verifying nothing broke. Never run composer update on a production server — always test the updated versions locally first.

If two plugins both include a library like Guzzle under its original GuzzleHttp namespace, only one version can be loaded — whichever plugin's autoloader fires first wins, and the other plugin may break or behave incorrectly. The solution is namespace prefixing: before distributing a plugin, rewrite all third-party namespaces to include your own prefix (e.g. GuzzleHttpMyPluginDepsGuzzleHttp). Two tools automate this: PHP-Scoper, which produces a separate prefixed vendor directory, and Mozart (the coenjacobs/mozart Composer plugin), which rewrites namespaces in-place after composer install. Namespace prefixing is essential for any plugin distributed to the public or installed alongside third-party plugins.

WPackagist (wpackagist.org) is a Composer repository that mirrors the entire WordPress.org plugin and theme directory. It lets you install WordPress plugins and themes as Composer packages using the wpackagist-plugin/ and wpackagist-theme/ prefixes. This is most useful in full-site Composer setups (like Roots Bedrock), where WordPress core, plugins, and themes are all managed as packages in a single root composer.json. For single-plugin development where you're just managing that plugin's own PHP library dependencies, WPackagist is not needed.

Add testing and tooling packages to require-dev rather than require. PHPUnit, PHP_CodeSniffer, Mockery, and Brain Monkey all belong in require-dev. When you run composer install (the default), dev dependencies are installed alongside runtime ones. When you run composer install --no-dev (what your CI/CD pipeline should use for the production artifact), dev packages are skipped entirely. This keeps PHPUnit out of the shipped plugin zip. Similarly, declare test class directories in autoload-dev rather than autoload so test classes aren't part of the production autoloader.

PSR-4 is the PHP standard that maps namespace prefixes to base directories. With "MyPlugin\": "src/" declared in composer.json, Composer's autoloader can find any class in the MyPlugin namespace by converting the namespace path to a file path: MyPluginAdminSettingssrc/Admin/Settings.php. The file is loaded automatically on first use — no require_once needed anywhere. For WordPress plugins, PSR-4 matters because it enforces a clean directory structure, enables dependency injection (classes are easy to instantiate with constructor arguments), makes unit testing straightforward (classes are isolated and importable in tests), and prevents the require_once spaghetti that plagues older WordPress codebases.

Photo of Ajay Khandal

Written by Ajay Khandal

I'm a freelance WordPress developer with 14+ years of experience building, fixing, and speeding up sites for businesses, agencies, and store owners across the US, UK, Europe, and Australia. I specialize in custom themes, WooCommerce, and performance — the kind of work that shows up as faster load times and fewer support tickets. No account managers, no outsourced tickets — you work directly with me, with replies typically inside 5 hours.

Work with me →