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
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\Settings → src/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 andusestatements, outputting a new directory (e.g.vendor-prefixed/) whereGuzzleHttpbecomesMyPlugin\Deps\GuzzleHttp. It’s thorough but has a steeper learning curve. - Mozart (
coenjacobs/mozart) — a Composer plugin that runs automatically aftercomposer installand rewrites namespaces in-place insidevendor/. 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
composer.jsonlives inside the plugin/theme directory, never in the WordPress root.- Runtime dependencies go in
require; testing and linting tools go inrequire-dev. - PHP version constraint is explicit:
"php": ">=8.1". - Autoloader uses PSR-4 with a unique namespace prefix to avoid collisions.
composer.lockis committed to version control.vendor/is committed for distributable plugins; gitignored and rebuilt by CI for private deployments.- Main plugin file includes the autoloader with a
file_exists()guard. - Production build uses
composer install --no-dev --optimize-autoloader. - Third-party library namespaces are prefixed (PHP-Scoper or Mozart) for publicly distributed plugins.
composer updateis 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.


