Contents
Introduction
Purpose: This article explains, in exhaustive detail, how to apply PSR-4 autoloading in a WordPress plugin or theme. It covers recommended project structure, composer configuration, the plugin/theme bootstrap code, fallback autoloaders for environments without Composer, optimization and deployment strategies, debugging tips, and common pitfalls. Wherever examples are used, complete code samples are provided.
What is PSR-4 and why use it in WordPress?
PSR-4 is a PHP autoloading standard defined by the PHP-FIG group. It maps namespaces to filesystem directories so classes are loaded on demand. Using PSR-4 in WordPress development yields:
- Cleaner code organization via namespaces and directory structure
- Lazy loading of classes (faster initial requests)
- Compatibility with Composer and third-party libraries
- Reduced naming collisions by using unique namespaces
- Better maintainability and easier unit testing
Overview of the approach
Two common setups:
- Composer managed — include Composers vendor/autoload.php in your plugin/theme entry file. Recommended when you or your build process can run Composer.
- Manual PSR-4 shim — provide a compact PSR-4 implementation (spl_autoload_register) in the plugin/theme for environments without Composer support (e.g., some installs where the vendor folder is not distributed). Use this only when necessary.
Naming and namespace recommendations
- Pick a top-level namespace that matches your vendor or plugin slug, for example: AcmeMyPlugin or AcmeThemeName.
- Keep namespaces consistent with directory structure (PSR-4 requirement).
- Avoid using WordPress core class names or global function names — use namespaces and class names to prevent collisions.
- Use PascalCase for class names and StudlyCaps for namespaces.
Recommended project structures
Plugin (preferred)
my-plugin/ ├─ composer.json ├─ vendor/ (generated by Composer) ├─ src/ │ ├─ Admin/ │ │ └─ SettingsPage.php │ ├─ Public/ │ │ └─ Assets.php │ └─ Main.php ├─ includes/ │ └─ helpers.php ├─ my-plugin.php (plugin bootstrap, plugin header) └─ README.md
Theme (preferred)
my-theme/ ├─ composer.json ├─ vendor/ ├─ inc/ │ ├─ Setup.php │ ├─ Customizer/ │ └─ Shortcodes/ ├─ functions.php (theme bootstrap) └─ style.css
Composer: composer.json examples
Your composer.json needs a PSR-4 section that maps a namespace prefix to the directory containing your PHP classes.
Plugin example composer.json
{ name: acme/my-plugin, description: My Plugin using PSR-4 autoloading, type: wordpress-plugin, license: GPL-2.0-or-later, autoload: { psr-4: { AcmeMyPlugin: src/ } }, require: { php: >=7.4 } }
Theme example composer.json
{ name: acme/my-theme, type: wordpress-theme, autoload: { psr-4: { AcmeMyTheme: inc/ } }, require: { php: >=7.4 } }
Installing composer and generating the autoloader
From the project root run:
composer install
When developing, use:
composer dump-autoload -o
Notes:
- composer install creates vendor/autoload.php.
- composer dump-autoload -o (or –optimize) builds an optimized class map for faster production autoloading.
- You can also use composer dump-autoload –classmap-authoritative to force Composer to use classmap only — useful for performance in certain deployments.
Plugin bootstrap: including Composers autoloader
Your plugin’s main file (the entry point with plugin header) should include vendor/autoload.php early, and then bootstrap your main class. Example:
init() } add_action( plugins_loaded, acme_myplugin_init )
Main class example (singleton pattern)
Theme bootstrap: including autoloader in functions.php
init() } )PSR-4 class example and file placement
Given composer.json mapping AcmeMyPlugin => src/, the following class:
Settings } }
must be placed at src/Admin/SettingsPage.php. PSR-4 requires that the namespace prefix maps to a base directory and that subsequent namespace parts map to subdirectories with the final class name matching the filename.
Common composer autoload options and when to use them
Option | Purpose |
---|---|
psr-4 | Primary mapping for modern autoloading — recommended. |
classmap | Scan directories and create class map useful for legacy code or when files do not match PSR-4 layout. |
files | Force include of files on every request — use sparingly (e.g., polyfills, procedural functions). |
Autoloader fallback: a small PSR-4 compatible autoloader (no Composer)
If you must ship without the vendor folder, include a compact autoloader that implements PSR-4 style logic. Keep it minimal and well-tested prefer Composer when possible.
Notes on the fallback autoloader
- This implementation supports only a single namespace prefix. Extend it to handle multiple prefixes if required.
- It does not include Composer features like PSR-0, classmap scanning, or optimized classmap — only simple PSR-4 logic.
- Only use this when you cannot provide a vendor directory Composer is preferred.
Dealing with dependencies (third-party libraries)
If your plugin or theme uses third-party packages via Composer, install them as normal with composer require. They will be autoloaded by the same vendor/autoload.php.
composer require guzzlehttp/guzzle
Composer ensures that multiple plugins/themes requiring the same package will share the same autoloader if they are loaded from the same vendor path — but in WordPress plugins each plugin typically ships its own vendor folder, which can duplicate dependencies. Strategies:
- Isolate vendor by using unique namespaces or prefixing (not recommended for common libraries).
- Defer to a shared implementation loaded by a mu-plugin or custom autoloader (advanced).
- Use Composers replace or conflict fields if you control multiple packages.
Autoload optimization and production deployment
- Run composer install --no-dev --prefer-dist during CI/deploy to avoid dev dependencies.
- Run composer dump-autoload -o to optimize autoloader for production.
- Consider committing vendor/ only if you cannot run Composer on the production environment. If you commit vendor/, keep it updated and audited.
- Use --classmap-authoritative for highest performance if your codebase does not require dynamic class discovery.
Performance considerations
- PSR-4 autoloading is fast and lazy — classes are loaded only when used.
- Optimized classmaps reduce filesystem checks. Use composer dump-autoload -o for production.
- Avoid excessive use of files autoloaded via files in composer.json, since they are included on every request.
- Combine autoloader optimization with PHP OPcache for best runtime performance.
Unit testing and CI
- Use autoloading in tests to load production classes. Include vendor/autoload.php in test bootstrap.
- Configure PHPUnit to autoload vendor/autoload.php and your test autoloader if needed.
- Run composer install in CI and prefer caching composer packages to speed builds.
WordPress-specific tips and edge cases
- Plugin header: Keep the plugin bootstrap file as lightweight as possible. Include autoload.php then delegate to your main class.
- Activation/Deactivation hooks: Keep these short and located in globally accessible functions or static methods hooks run before autoloading sometimes — ensure classes used in activation hooks are loaded (manually require their files or use functions in the main plugin file that call class methods after autoloader is loaded).
- Must-Use (mu-) plugins: mu-plugins are usually single-file. If packaging with Composer you either: ship a mu-plugin that requires a vendor/autoload.php in a fixed location, or create a single-file mu-plugin that contains an extracted autoloader and your code.
- Theme requirements: When autoloading from a theme, the themes functions.php should require vendor/autoload.php before referencing namespaced classes.
- Multisite: Keep autoload logic identical just ensure paths (use __DIR__) are correct. For network-activated plugins, the autoloader inclusion is the same.
Handling plugin activation edge cases
If your activation hook needs to instantiate classes that are PSR-4 loaded, ensure the autoloader is available before the hook runs. Example pattern:
Debugging autoloading problems
- Check the namespace, folder, and filename match PSR-4 mapping exactly (case sensitive on some filesystems).
- Verify vendor/autoload.php exists and is required before using namespaced classes.
- Use composer dump-autoload -o to rebuild mappings and flush caches if class not found.
- Enable WP_DEBUG and check PHP error logs for autoload errors.
Advanced: multiple namespace mappings and packages
You can map multiple namespaces or sub-namespaces to different directories in composer.json. Example:
{ autoload: { psr-4: { AcmeMyPlugin: src/, AcmeMyPluginTests: tests/ } } }
Keep test autoloading separate and restrict tests to dev dependencies with composer dev autoload configuration.
Advanced: combining PSR-4 with classmap and files
For legacy or procedural code you can mix autoload strategies:
{ autoload: { psr-4: { AcmeMyPlugin: src/ }, classmap: [ includes/ ], files: [ includes/helpers.php ] } }
Use files sparingly because each entry is included on every request.
Security considerations
- Keep vendor packages updated and scan for vulnerabilities.
- Do not expose sensitive data in composer.json or other files shipped with the plugin/theme.
- Validate any code or data included dynamically do not include files based on user input.
Deployment checklist
- Run tests in CI and ensure autoload is working in test environment.
- composer install --no-dev --prefer-dist
- composer dump-autoload -o
- Verify vendor/autoload.php is present in the packaged plugin/theme or provide a build step that creates a distributable artifact.
- Confirm plugin bootstrap includes vendor/autoload.php with a file_exists check to avoid fatal errors if missing.
- Consider using semantic versioning and tagging releases.
Complete real-world plugin example (all pieces together)
Directory: my-plugin/
my-plugin/ ├─ composer.json ├─ vendor/ ├─ src/ │ ├─ Main.php │ └─ Admin/ │ └─ SettingsPage.php ├─ my-plugin.php └─ includes/ └─ autoload.php (optional fallback)
composer.json (already shown earlier). Now the files:
init() } else { // Log or handle the missing class situation. error_log( My Plugin: Main class not found. ) } } )
My Plugin Settings