Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 094 / 100

Dependency management in plugins for WordPress developers

Use Composer without conflicts.

Expona Admin7 min read

Introduction

You've built a solid WordPress plugin that works beautifully in your test environment. Then a client installs it alongside another plugin, and suddenly things break—usually with a cryptic fatal error about a missing class or a namespace collision. Dependency management in plugins for WordPress developers is often the culprit, and it's a problem that grows the more you rely on external libraries. Managing those dependencies well means the difference between a plugin that plays nicely in any WordPress installation and one that becomes a support nightmare.

We're on Day 94 of the WordPress development series, and at this point in a plugin's lifecycle, you're likely integrating third-party code—whether that's an HTTP client, a templating engine, or a validation library. The challenge isn't using those libraries; it's using them safely so your plugin doesn't conflict with every other plugin in the ecosystem.

Composer autoloading — Load your classes efficiently

Composer has become the standard for PHP dependency management, and WordPress plugins are no exception. Instead of manually requiring files or rolling your own autoloader, Composer gives us PSR-4 autoloading, which means your classes are loaded on demand and your plugin stays lightweight.

Here's a typical plugin structure with Composer:

php
├── my-plugin/
│   ├── src/
│   │   ├── Admin/
│   │   ├── Frontend/
│   │   └── Core/
│   ├── vendor/
│   ├── composer.json
│   ├── composer.lock
│   └── my-plugin.php

Your composer.json should define a PSR-4 namespace for your own code:

php
{
  "name": "mycompany/my-plugin",
  "autoload": {
    "psr-4": {
      "MyCompany\\MyPlugin\\": "src/"
    }
  },
  "require": {
    "php": ">=7.4",
    "guzzlehttp/guzzle": "^7.0"
  }
}

Then in your main plugin file, you load the Composer autoloader once:

php

This approach means you're not manually requiring files. Classes load when you use them, and your plugin stays performant. The autoloader respects namespaces, so a class in src/Admin/Settings.php is automatically available as MyCompany\MyPlugin\Admin\Settings.

Without Composer autoloading, you'd either be manually requiring every file (messy and slow) or using a custom autoloader that you maintain yourself (extra burden). Composer takes that off your plate.

Conflicts — Two plugins bundling different versions of a library

Here's the scenario that keeps WordPress developers up at night: Plugin A bundles Guzzle 7.4, and Plugin B bundles Guzzle 6.5. Both plugins load their vendor directories. WordPress loads them in some order, and suddenly one plugin's code is calling methods that don't exist in the other version.

This happens because plugins typically commit their entire vendor/ directory to their repository and load it unconditionally. There's no coordination between plugins, no version negotiation, and no fallback mechanism. The last plugin to load wins, but its code might be calling methods from the version that's not actually loaded.

Real-world example: You're using an HTTP library that changed its API between versions. Plugin A expects $client->request('GET', $url), but Plugin B loaded a version that only has $client->get($url). Even though both versions of the library are in separate vendor/ directories, only one gets executed—usually whichever is first in PHP's include path.

The fundamental issue is that global namespace collisions are unavoidable when multiple plugins load different versions of the same library. If both plugins load \GuzzleHttp\Client, PHP only knows about one of them at runtime. This is why WordPress core recommends against bundling third-party code at all, but in practice, most plugins do.

The solution—and we'll explore it in depth next—is to isolate your dependencies so they don't pollute the global namespace.

Scoping — PHP-Scoper or Strauss prefix dependencies

Scoping means prefixing all the namespaces in your dependencies so they become unique to your plugin. Instead of \GuzzleHttp\Client, it becomes \MyCompany\MyPlugin\Vendor\GuzzleHttp\Client. Now even if another plugin loads Guzzle, there's no collision because you're using a separate namespace.

PHP-Scoper is the most robust tool for this. It parses your vendor code and rewrites all namespaces and class names. Here's how you'd set it up:

First, install PHP-Scoper as a dev dependency:

php
composer require-dev humbug/php-scoper

Create a scoper.inc.php in your plugin root:

php
 'MyCompany\\MyPlugin\\Vendor',
    'exclude-files' => [],
    'exclude-namespaces' => [
        // Don't scope your own code
        'MyCompany\\MyPlugin',
    ],
    'exclude-classes' => [],
    'exclude-functions' => [],
    'exclude-constants' => [],
    'patchers' => [],
];

Then add a build script to your composer.json:

php
{
  "scripts": {
    "build": [
      "php-scoper add-prefix --output-dir=vendor-scoped --force",
      "rm -rf vendor",
      "mv vendor-scoped vendor"
    ]
  }
}

When you run composer run build, PHP-Scoper rewrites every class in your vendor directory. Your autoloader still works—it's just pointing to prefixed classes now:

php
// After scoping, this becomes:
use MyCompany\MyPlugin\Vendor\GuzzleHttp\Client;

$client = new Client();

Strauss is a lighter alternative if you want to scope just a few packages rather than the entire vendor directory. It's easier to set up but gives you less control.

The trade-off: Scoping adds a build step to your workflow. You can't just run composer install and commit—you need to run the scoper and commit the scoped vendor directory. For a production plugin, this is worth it. For internal tools or small projects, it might feel like overkill.

A practical middle ground: Only scope the dependencies that are most likely to conflict. If you're using a popular HTTP library like Guzzle, scope it. If you're using a niche utility library you wrote yourself, you probably don't need to.

Keep it small — Fewer dependencies, fewer conflicts

The simplest dependency management strategy is to have as few dependencies as possible. Every library you add is a potential conflict, a maintenance burden, and a security risk if it's not kept up to date.

Before you reach for Composer, ask: Do I actually need this? WordPress ships with some powerful built-in tools that often go underutilized. wp_remote_get() and wp_remote_post() handle HTTP requests. The sanitize_* and validate_* functions cover validation. wp_kses() handles HTML sanitization. For many plugins, the WordPress API is sufficient.

If you do need an external library, prefer libraries with minimal dependencies themselves. A library that depends on fifteen other packages creates a larger surface area for conflicts. Check the composer.json of any package you're adding, and prefer those with few or no dependencies.

Example: You need to parse CSV files. You could use league/csv, which has zero dependencies. Or you could use a heavier data processing library that pulls in a dozen other packages. The simpler choice is almost always better for a plugin.

Version constraints matter too. In your composer.json, use conservative version constraints:

php
// Good: Patch updates only
"guzzlehttp/guzzle": "^7.4.5"

// Riskier: Minor updates allowed, might break things
"guzzlehttp/guzzle": "^7.0"

// Very risky: Any future version
"guzzlehttp/guzzle": "*"

The more restrictive you are, the less likely a dependency update will introduce a breaking change. But you also need to stay current on security patches, so this requires ongoing maintenance.

Common mistakes

  • Forgetting to load the autoloader: Many developers add dependencies via Composer but forget to require_once the autoloader in their main plugin file. The classes exist but aren't loaded, resulting in "class not found" errors.
  • Loading the autoloader multiple times: If multiple files in your plugin load the autoloader, it's redundant and slows things down. Load it once in your entry point.
  • Committing vendor to Git without scoping: This works in isolation but scales poorly. A better approach is to either scope dependencies or use Composer's require-dev for build tools and require only your code in production.
  • Assuming WordPress will handle dependency resolution: WordPress has no mechanism for resolving conflicting library versions. You have to solve this yourself with scoping or by minimizing dependencies.
  • Not running composer update regularly: If your dependencies have security vulnerabilities, you need to know about them. Use tools like Dependabot to stay on top of updates.

Key takeaways

  • Use Composer with PSR-4 autoloading to load your plugin classes efficiently and avoid manual requires.
  • Namespace collisions happen when multiple plugins bundle different versions of the same library—the global namespace can only hold one.
  • Scope your dependencies with PHP-Scoper or Strauss to prefix all namespaces and eliminate conflicts, even if it adds a build step.
  • The best conflict is the one you never have: minimize dependencies, prefer WordPress built-ins when possible, and be intentional about what you add.
  • If you do bundle dependencies, scope them. If you scope them, you own the build process and the maintenance that comes with it.
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