As WordPress projects grow in complexity, organizing your codebase becomes essential. Whether you’re building a plugin, theme, or block library, a clean and scalable architecture makes development faster, onboarding easier, and long-term maintenance far less painful.
In Refactoring the Multi-Block Plugin: Build Smarter, Register Cleaner, Scale Easier, I walked through setting up a flexible structure for managing multiple static, dynamic, and interactive blocks within a single plugin. That setup serves as the foundation for this guide.
Here, we’ll take things a step further by introducing PHP namespaces, autoloading with Composer, and enforcing consistent coding standards across JavaScript, CSS, and PHP. These aren’t just best practices — they’re practical steps for maintaining quality and scaling your code as your project grows.
We’ll walk through:
- Setting up PSR-4 autoloading with Composer
- Structuring plugin functionality into reusable, purpose-driven classes
- Enforcing consistent styles with automated linting and formatting tools
This workflow is flexible enough for solo developers, yet robust enough to support team collaboration on large-scale projects.
Table of Contents
Pre-requisites
This article builds on Refining a Multi-Block Plugin. If you’ve already followed that guide to scaffold a plugin with static, dynamic, and interactive blocks, you’re ready to dive into this next step.
Prefer to skip the setup? The full plugin built in this article is available on GitHub with each section of this article is represented in a branch.
Want to follow along but don’t want to build the multi-block start point? You can clone the multi-block repo, run npm install and start from there.
Either approach will give you a ready-to-use starting point with namespacing, Composer autoloading, and linting already wired up.
Namespacing and classes
As the plugin grows, it’s important to keep the code organized and easy to manage. I use PHP namespaces and split functionality into classes so each part of the plugin has a clear purpose. This helps avoid naming conflicts and makes the codebase easier to scale and maintain.
Composer and autoloading
To streamline how classes are loaded, I use Composer with PSR-4 autoloading. This means I don’t need to manually include files, Composer automatically loads classes based on the namespace and folder structure I define.
In this setup, I use the namespace Advanced_Multi_Block to group all plugin-related classes. This acts like a prefix, helping avoid naming conflicts and keeping everything contained within the plugin’s scope.
I start by creating a composer.json file in the plugin root and adding the following:
{
"name": "multi-block/namespacing-coding-standards",
"description": "An advanced multi block plugin with custom functionality.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"autoload": {
"psr-4": {
"Advanced_Multi_Block\\": "Functions/"
}
}
}
Then I run: composer install. This generates the vendor folder and an autoloader that I can include in the main plugin file.
Files and classes
With autoloading in place, I organize all class-based PHP files into a Functions folder inside the plugin. This keeps everything in one place and makes it easier to manage as the plugin grows.
Each class handles a specific piece of functionality and is namespaced under Advanced_Multi_Block. This structure helps keep responsibilities clear and avoids naming conflicts across the codebase.
Plugin Paths
This class provides helper methods for getting the plugin’s base path and URL. I use it throughout the plugin to avoid repeating logic or relying on hardcoded values.
Inside the Functions directory I create Plugin_Paths.php and paste in the following code:
<?php
namespace Advanced_Multi_Block;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Plugin_Paths {
public static function plugin_url() {
return plugin_dir_url( dirname( __FILE__ ) );
}
public static function plugin_path() {
return plugin_dir_path( dirname( __FILE__ ) );
}
}
Register Blocks
This class handles the logic for finding and registering blocks. I hook into the init action in the constructor, and use Plugin_Paths to get the plugin’s path.
The register_blocks() method loops through each block directory, checks for a block.json file, and registers the block. If the block includes a viewScriptModule field, it adds a filter so WordPress loads the required assets for interactive blocks.
Inside the Functions directory I create Register_Blocks.php and paste in the following code:
<?php
namespace Advanced_Multi_Block;
use Advanced_Multi_Block\Plugin_Paths;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Register_Blocks {
public function __construct() {
add_action( 'init', array( $this, 'register_blocks' ) );
}
public function register_blocks() {
if ( function_exists( 'wp_register_block_types_from_metadata_collection' ) ) {
wp_register_block_types_from_metadata_collection( Plugin_Paths::plugin_path() . 'build/blocks', Plugin_Paths::plugin_path() . '/build/blocks-manifest.php' );
return;
}
if ( function_exists( 'wp_register_block_metadata_collection' ) ) {
wp_register_block_metadata_collection( Plugin_Paths::plugin_path() . 'build/blocks', Plugin_Paths::plugin_path() . '/build/blocks-manifest.php' );
}
$manifest_data = include Plugin_Paths::plugin_path() . 'build/blocks-manifest.php';
foreach ( array_keys( $manifest_data ) as $block_type ) {
register_block_type( Plugin_Paths::plugin_path() . "build/blocks/{$block_type}" );
}
}
}
Enqueue
This class registers two global asset entry points, one for the editor and one for the frontend. These scripts are separate from block-specific scripts and are useful for features like block variations, editor UI enhancements, or global behavior that spans multiple blocks.
Each script uses its corresponding .asset.php file to ensure dependencies and versioning are handled correctly. I use the Plugin_Paths helper to reference the correct paths and URLs.
Inside the Functions directory, I create a file named Enqueues.php and add the following code:
<?php
namespace Advanced_Multi_Block;
use Advanced_Multi_Block\Plugin_Paths;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Enqueues {
public function __construct() {
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
}
/**
* Enqueues the block assets for the editor
*/
public function enqueue_block_assets() {
$asset_file = include Plugin_Paths::plugin_path() . 'build/editor-script.asset.php';
wp_enqueue_script(
'editor-script-js',
Plugin_Paths::plugin_url() . 'build/editor-script.js',
$asset_file['dependencies'],
$asset_file['version'],
false
);
}
/**
* Enqueues the block assets for the frontend
*/
public function enqueue_frontend_assets() {
$asset_file = include Plugin_Paths::plugin_path() . 'build/frontend-script.asset.php';
wp_enqueue_script(
'frontend-script-js',
Plugin_Paths::plugin_url() . 'build/frontend-script.js',
$asset_file['dependencies'],
$asset_file['version'],
true
);
}
}
Updating main plugin file
With the classes and autoloading in place, I update the main plugin file to load everything properly. First, I check for the Composer autoload file and require it. Then I instantiate the core classes so their functionality is registered with WordPress.
This setup keeps the main file clean and focused while letting each class handle its own responsibilities.
<?php
if (! defined('ABSPATH') ) {
exit;
}
// Include Composer's autoload file.
if ( file_exists( plugin_dir_path( __FILE__ ) . 'vendor/autoload.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
} else {
wp_trigger_error( 'Advanced Multi Block Plugin: Composer autoload file not found. Please run `composer install`.', E_USER_ERROR );
return;
}
// Instantiate the classes.
$advanced_multi_block_classes = array(
\Advanced_Multi_Block\Plugin_Paths::class,
\Advanced_Multi_Block\Register_Blocks::class,
\Advanced_Multi_Block\Enqueues::class,
);
foreach ( $advanced_multi_block_classes as $advanced_multi_block_class ) {
new $advanced_multi_block_class();
}
Coding standards
Keeping code consistent is one of the easiest ways to improve collaboration and long-term maintainability, especially in larger or shared codebases. This section shows how I set up JavaScript, CSS, and PHP linting and formatting for this plugin using WordPress’s recommended tools.
Note: This isn’t meant to be a strict rule set. It’s a working starting point you can use as-is or adapt to fit your team’s preferences.
Add linting for JS
To enforce consistent code across my JavaScript files, I set up linting with ESLint and formatting with Prettier using the WordPress recommended configurations.
I install the required development dependencies:
npm install --save-dev @wordpress/eslint-plugin eslint-config-prettier @wordpress/prettier-config eslint-config-prettier
Then I create a .eslintrc.json file at the root of the plugin with the following configuration. It extends the recommended rules from the WordPress ESLint plugin and sets up a few environment and parser options:
{
"extends": ["plugin:@wordpress/eslint-plugin/recommended"],
"env": {
"browser": true,
"es6": true,
"jquery": true
},
"parserOptions": {
"requireConfigFile": false,
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
"@wordpress/no-global-active-element": "warn",
"@wordpress/no-unsafe-wp-apis": "warn"
}
}
I also add a .eslintignore file to prevent linting of compiled and third-party files:
/build/
/vendor/
/node_modules/
*.css
*.scss
To handle code formatting, I create a .prettierrc file with my preferred style rules:
{
"tabWidth": 4,
"useTabs": true,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"semi": true,
"bracketSameLine": false,
"jsxSingleQuote": false,
"jsxBracketSameLine": false
}
I also create a .prettierignore file to exclude generated and dependency directories:
build
node_modules
vendor
In package.json, I add the following scripts to lint and format JS files. These replace any existing lint:js or format:js entries:
"lint:js": "wp-scripts lint-js --max-warnings=0",
"format:js": "wp-scripts lint-js --fix",
I can now run npm run lint:js or npm run format:js to lint and fix JavaScript files.
Add linting for CSS
To ensure consistency and catch potential issues in my SCSS files, I configure Stylelint using WordPress’s recommended configuration for SCSS.
I install the necessary development dependencies:
npm install --save-dev @wordpress/stylelint-config stylelint stylelint-scss
Then I create a .stylelintrc.json file at the root of the plugin with the following configuration. This extends the WordPress SCSS rules and disables a couple of rules to match my preferred style:
{
"extends": ["@wordpress/stylelint-config/scss"],
"rules": {
"at-rule-no-unknown": null,
"selector-class-pattern": null,
"scss/at-rule-no-unknown": true
}
}
I also create a .stylelintignore file to exclude compiled and vendor files:
build/
node_modules/
vendor/
*.min.css
*.min.scss
In package.json, I add the following scripts to lint and fix SCSS files. These replace any existing lint:css or format:css entries:
"lint:css": "stylelint \"**/*.scss\" --max-warnings=0",
"format:css": "stylelint \"**/*.scss\" --fix",
I can now run npm run lint:css or npm run format:css to lint and fix CSS files.
Add linting for PHP
To enforce WordPress coding standards in my PHP files, I use PHP_CodeSniffer with the WordPress Coding Standards (WPCS) ruleset. This setup helps catch common issues and maintain consistency across the plugin.
I start by creating a phpcs.xml.dist file in the plugin root with the following configuration:
<?xml version="1.0"?>
<ruleset name="WordPress Plugin Coding Standards">
<description>A custom set of rules to check for a WordPress plugin</description>
<!-- What to scan -->
<file>.</file>
<exclude-pattern>/build/</exclude-pattern>
<exclude-pattern>/node_modules/</exclude-pattern>
<exclude-pattern>/vendor/</exclude-pattern>
<exclude-pattern>src/blocks-manifest.php</exclude-pattern>
<exclude-pattern>build/*.asset.php</exclude-pattern>
<!-- How to scan -->
<arg value="sp"/>
<arg name="basepath" value="."/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="4"/>
<!-- Rules: WordPress Coding Standards -->
<config name="minimum_supported_wp_version" value="6.6"/>
<rule ref="WordPress">
<exclude name="Generic.Arrays.DisallowShortArraySyntax"/>
<exclude name="Generic.Functions.CallTimePassByReference"/>
<exclude name="WordPress.PHP.YodaConditions.NotYoda"/>
</rule>
<rule ref="WordPress.Arrays.MultipleStatementAlignment">
<properties>
<property name="maxColumn" value="80"/>
</properties>
</rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="Advanced_Multi_Block"/>
</property>
</properties>
</rule>
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
<element value="advanced-multi-block"/>
</property>
</properties>
</rule>
</ruleset>
This is a starting point based on the official WordPress standards. I’d recommend reviewing the full WPCS documentation if you want to customize it further.
Next, I update my composer.json file to install the required dependencies and define scripts for linting and formatting:
{
"name": "wp-dev-blog/refactor-multi-block-plugin",
"description": "An advanced multi block plugin with custom functionality.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"autoload": {
"psr-4": {
"Advanced_Multi_Block\\": "Functions/"
}
},
"require-dev": {
"wp-coding-standards/wpcs": "^3.1"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
"format": "./vendor/bin/phpcbf --report-summary --report-source || true",
"lint": "./vendor/bin/phpcs"
}
}
Then I run: composer update to install the new packages.
In package.json, I add the following scripts to lint and fix PHP files. These replace any existing lint:php or format:php entries:
"lint:php": "composer run lint",
"format:php": "phpcbf --standard=phpcs.xml.dist -v",
I can now run npm run lint:php or npm run format:php to lint and fix PHP files.
Global commands
To keep all linting commands consistent across environments, I add the following to package.json:
"lint": "npm run lint:js && npm run lint:php && npm run lint:css",
"format": "npm run format:js && npm run format:php && npm run format:css",
Autoloading with Composer
With Composer’s PSR-4 autoloading defined in composer.json, you don’t need manual require statements for your classes. The main plugin file loads the generated vendor/autoload.php, and Composer handles the rest.
Whenever you add, move, or rename classes, run: composer dump-autoload
This step refreshes the class map so new or updated files are discoverable. It’s an easy but important habit that prevents “class not found” errors and keeps your plugin’s structure in sync with your namespaces.
Conclusion
By adding namespacing, Composer autoloading, and clear coding standards, you create a foundation that’s built to last. Your code becomes easier to reason about, test, and extend — and it stays that way as your plugin or theme grows.
Each component of this setup plays a part:
- Namespaces and classes separate concerns and reduce conflicts
- Composer autoloading eliminates boilerplate and scales effortlessly
- Linting and formatting tools keep your code clean and consistent across the entire stack
This approach isn’t just about polish, it’s about building with confidence and avoiding slowdowns as your codebase evolves. Whether you’re building for clients, teams, or yourself, these practices help ensure the project remains maintainable, performant, and collaborative.
As always, feedback is welcome. If you have suggestions, questions, or want to share how you’re structuring your own projects, I’d love to hear from you.
Props to @meszarosrob and @milana_cap for reviewing this article and offering feedback.
Leave a Reply