Skip to content

Proposal: Simplify namespace conflict prevention with class_exists() guard #14

@mikelittle

Description

@mikelittle

Is your enhancement related to a problem? Please describe.

In my composer-based project requiring a library that requires 10up/simple-local-avatars as a dependency, composer will install that plugin in the vendor/ folder. It will also install this library in the vendor folder, ignoring the 'installers' configuration, and will not run the 'post-install' command to rename the namespace. Both those steps are a security risk for indirect dependencies: composer will never run them.

I understand the idea of these steps is to allow multiple plugins to include this library without running into namespace clashes.

I'd like to propose a simpler approach to preventing namespace conflicts when multiple WordPress plugins use this library. Instead of requiring each plugin to run the replace-namespace.sh script to create unique namespaces, we can add a class_exists() guard that allows all plugins to safely share the same namespace.

Current Approach

The library currently recommends that each plugin using it:

  1. Configure Composer post-install/post-update hooks
  2. Run replace-namespace.sh to replace WP_Compat_Validation_Tool with a unique namespace
  3. Use custom installer paths to place the library in 10up-lib/

While this ensures complete isolation, it adds complexity to the integration process and results in duplicate code when multiple plugins use the library.

Proposed Approach

Add a class_exists() check around the class definition:

<?php
namespace WP_Compat_Validation_Tool;

if ( ! class_exists( __NAMESPACE__ . '\Validator' ) ) {

class Validator {
    // ... existing code unchanged ...
}

} // End class_exists check

This allows:

  • Multiple plugins to require_once the file without conflicts
  • The first plugin to load the class "wins"
  • Subsequent plugins safely skip re-defining the class
  • All plugins share the same WP_Compat_Validation_Tool\Validator class

Why This Works for This Library

This approach is safe and appropriate for the Validator class because:

  1. Instance-based state: Each Validator instance maintains its own $checklist and $messages arrays. There's no shared/static state between plugins.

  2. Stable API: The class has a simple, stable fluent interface (set_plugin_name(), set_php_min_required_version(), etc.) that's unlikely to have breaking changes.

  3. Common WordPress pattern: This is the standard approach used by many shared WordPress libraries (CMB2, various utility libraries). It's well-tested in production across thousands of sites.

  4. Simple functionality: The class performs straightforward version comparisons. The logic is stable and unlikely to differ significantly between versions.

Comparison of Approaches

Aspect Namespace Replacement class_exists() Guard
Setup complexity High (Composer hooks, custom paths) Low (standard Composer require)
Code duplication Each plugin has full copy Single shared class
Version isolation Complete "First loader wins"
Runtime overhead None Minimal (single function call)
Maintenance Script must be run on updates Automatic

Trade-offs

The main trade-off is version control:

  • With namespace replacement, each plugin is guaranteed to use its bundled version
  • With class_exists(), whichever plugin loads first determines the class definition

For this library, this trade-off is acceptable because:

  • The API is simple and stable
  • Minor version differences won't break compatibility
  • This is a pre-boot validation tool, not core business logic

Backward Compatibility

The replace-namespace.sh script can remain available for developers who prefer strict version isolation. The documentation can present class_exists() as the default (simpler) approach while documenting namespace replacement as an advanced option.

Proposed Changes

  1. src/Validator.php: Add class_exists() guard around the class definition
  2. README.md: Simplify default setup instructions; document namespace replacement as optional

Example Updated Usage

Standalone plugins (in wp-content/plugins/):

if ( ! is_readable( __DIR__ . '/vendor/10up/wp-compat-validation-tool/src/Validator.php' ) ) {
    return;
}

require_once __DIR__ . '/vendor/10up/wp-compat-validation-tool/src/Validator.php';

$compat_checker = new \WP_Compat_Validation_Tool\Validator();
$compat_checker
    ->set_plugin_name( 'My Plugin' )
    ->set_php_min_required_version( '7.4' );

if ( ! $compat_checker->is_plugin_compatible() ) {
    return;
}

// Safe to load other dependencies
require_once __DIR__ . '/vendor/autoload.php';

Dual-distribution plugins (standalone + Composer):

Many plugins are distributed both as standalone WordPress plugins (with bundled vendor/) and via Composer from GitHub. This pattern handles both:

// 1. Try to load from bundled vendor (standalone installs)
$bundled = __DIR__ . '/vendor/10up/wp-compat-validation-tool/src/Validator.php';
if ( is_readable( $bundled ) ) {
    require_once $bundled;
}

// 2. Check if class is available (autoloader may have loaded it in Composer setups)
if ( ! class_exists( '\WP_Compat_Validation_Tool\Validator' ) ) {
    return; // Library not available
}

// 3. Safe to use
$compat_checker = new \WP_Compat_Validation_Tool\Validator();
$compat_checker
    ->set_plugin_name( 'My Plugin' )
    ->set_php_min_required_version( '7.4' );

if ( ! $compat_checker->is_plugin_compatible() ) {
    return;
}

This works for:

  • Standalone: Loads from bundled vendor/
  • Composer (plugin commits vendor/): Loads bundled copy, class_exists() skips if autoloader already loaded it
  • Composer (plugin doesn't commit vendor/): Bundled path fails silently, autoloader provides the class

Composer-only plugins (in app's vendor/ folder):

When a plugin is exclusively a Composer dependency, just use the class directly:

// The autoloader handles loading - class_exists() guard prevents conflicts
$compat_checker = new \WP_Compat_Validation_Tool\Validator();
$compat_checker
    ->set_plugin_name( 'My Plugin' )
    ->set_php_min_required_version( '7.4' );

if ( ! $compat_checker->is_plugin_compatible() ) {
    return;
}

This works because the app's autoloader is loaded early, and the library has minimal PHP requirements itself.

Mixed Environment Support

The class_exists() approach works correctly across all combinations:

Environment How Class is Loaded Conflicts?
Traditional plugin A require_once bundled copy No - defines class
Traditional plugin B require_once bundled copy No - class_exists() skips
Composer-managed plugin App's autoloader No - reuses existing or defines
Dual-distribution plugin (standalone) require_once bundled copy No - defines or skips
Dual-distribution plugin (in Composer) Tries bundled, falls back to autoloader No - works either way
Mixed (all above together) First loader wins No - all share same class

The class_exists() check operates at the PHP runtime level, regardless of whether the class was loaded via autoloader or require_once. Each plugin still gets its own instance with independent state.

No Composer hooks, no custom installer paths, no namespace replacement script - just a standard Composer dependency that works everywhere.

Questions for Maintainers

  1. Are there planned API changes that would make class_exists() problematic?
  2. Is there a strong preference for complete version isolation over simplicity?
  3. Would you accept a PR implementing this approach?

I'm happy to submit a PR with these changes if this approach is acceptable.


Labels to Add

  • enhancement
  • question

Patch (for reference)

Core Change: src/Validator.php

The key code change is minimal - just wrap the class in a class_exists() guard:

 <?php
 namespace WP_Compat_Validation_Tool;

+if ( ! class_exists( __NAMESPACE__ . '\Validator' ) ) {
+
 class Validator {
 	/**
 	 * Array of checks.
@@ -167,3 +169,5 @@ class Validator {
 		<?php
 	}
 }
+
+} // End class_exists check

Documentation: README.md

The README updates include:

  • Simplified default setup (no Composer hooks required)
  • Usage examples for standalone plugins (wp-content/plugins/)
  • Usage examples for Composer-managed plugins (in app's vendor/ folder)
  • Explanation of how the class_exists() guard enables safe sharing
  • Mixed environment compatibility table
  • Namespace replacement documented as optional advanced feature

Designs

No response

Describe alternatives you've considered

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

Labels

No labels
No labels
No fields configured for 💡 Idea / Suggestion.

Projects

Status
In Progress

Relationships

None yet

Development

No branches or pull requests

Issue actions