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:
- Configure Composer post-install/post-update hooks
- Run
replace-namespace.sh to replace WP_Compat_Validation_Tool with a unique namespace
- 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:
-
Instance-based state: Each Validator instance maintains its own $checklist and $messages arrays. There's no shared/static state between plugins.
-
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.
-
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.
-
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
src/Validator.php: Add class_exists() guard around the class definition
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
- Are there planned API changes that would make
class_exists() problematic?
- Is there a strong preference for complete version isolation over simplicity?
- 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
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
Is your enhancement related to a problem? Please describe.
In my composer-based project requiring a library that requires
10up/simple-local-avatarsas a dependency,composerwill install that plugin in thevendor/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:composerwill 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.shscript to create unique namespaces, we can add aclass_exists()guard that allows all plugins to safely share the same namespace.Current Approach
The library currently recommends that each plugin using it:
replace-namespace.shto replaceWP_Compat_Validation_Toolwith a unique namespace10up-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:This allows:
require_oncethe file without conflictsWP_Compat_Validation_Tool\ValidatorclassWhy This Works for This Library
This approach is safe and appropriate for the Validator class because:
Instance-based state: Each
Validatorinstance maintains its own$checklistand$messagesarrays. There's no shared/static state between plugins.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.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.
Simple functionality: The class performs straightforward version comparisons. The logic is stable and unlikely to differ significantly between versions.
Comparison of Approaches
class_exists()GuardTrade-offs
The main trade-off is version control:
class_exists(), whichever plugin loads first determines the class definitionFor this library, this trade-off is acceptable because:
Backward Compatibility
The
replace-namespace.shscript can remain available for developers who prefer strict version isolation. The documentation can presentclass_exists()as the default (simpler) approach while documenting namespace replacement as an advanced option.Proposed Changes
src/Validator.php: Addclass_exists()guard around the class definitionREADME.md: Simplify default setup instructions; document namespace replacement as optionalExample Updated Usage
Standalone plugins (in wp-content/plugins/):
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:This works for:
vendor/class_exists()skips if autoloader already loaded itComposer-only plugins (in app's vendor/ folder):
When a plugin is exclusively a Composer dependency, just use the class directly:
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:require_oncebundled copyrequire_oncebundled copyclass_exists()skipsrequire_oncebundled copyThe
class_exists()check operates at the PHP runtime level, regardless of whether the class was loaded via autoloader orrequire_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
class_exists()problematic?I'm happy to submit a PR with these changes if this approach is acceptable.
Labels to Add
enhancementquestionPatch (for reference)
Core Change: src/Validator.php
The key code change is minimal - just wrap the class in a
class_exists()guard:Documentation: README.md
The README updates include:
wp-content/plugins/)vendor/folder)class_exists()guard enables safe sharingDesigns
No response
Describe alternatives you've considered
No response
Code of Conduct