The five-minute install gets you a running site. It does not get you a developer workstation. In 2026 a solid WordPress developer setup is a local stack you can recreate, a lint pipeline that matches core, safe debugging that never paints errors on the front end, and block tooling that ships the same assets you test.
This guide is the checklist we use when provisioning a new theme or plugin repo: environment type, wp-env, @wordpress/scripts, debug constants, and a thin hardening layer in wp-config.php and mu-plugins.
How to set up wp-env for local WordPress development
Hand-rolled Apache and PHP installs drift. One machine runs PHP 8.2, another still has 8.1, and block builds fail only on CI. The official path is wp-env: Docker containers for WordPress, MySQL, and optional tools, driven from a .wp-env.json in the project root.
Install once globally or as a project dependency:
npm install --save-dev @wordpress/env
npx wp-env startA minimal config for a plugin under development:
{
"core": "WordPress/WordPress#6.7",
"plugins": [ "." ],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true,
"WP_DEBUG_DISPLAY": false,
"SCRIPT_DEBUG": true
}
}Why this beats a generic stack for plugin work:
- Core version is pinned in git, so “works on my machine” stops being a debate.
wp-env run cligives you WP-CLI inside the same containers your browser hits.- Tests and e2e scripts in
@wordpress/scriptsexpect this layout.
Pair it with the block editor development environment docs when you need Node version notes, create-block, and the recommended editor extensions.
For agency work that is still classic PHP themes, you can keep a host PHP binary for one-off scripts, but keep the site itself in wp-env so media, cron, and rewrite behaviour match staging.
Useful daily commands once the stack is up:
npx wp-env run cli wp plugin list
npx wp-env run cli wp cache flush
npx wp-env logs
npx wp-env stopMap a custom domain through your hosts file only when the project needs cookie or SSO behaviour that localhost breaks. Otherwise the default URL is enough and keeps certificates simple.
When a teammate clones the repo, the onboarding note should be three lines: install Docker, npm install, npx wp-env start. Anything longer usually means the environment escaped into undocumented host packages.
Set WP_ENVIRONMENT_TYPE and hardening constants in wp-config
Since WordPress 5.5, WP_ENVIRONMENT_TYPE is the switch every plugin should read. Define it early in wp-config.php:
define( 'WP_ENVIRONMENT_TYPE', 'local' ); // local | development | staging | productionThen branch behaviour without inventing your own constants:
if ( wp_get_environment_type() === 'production' ) {
// Caching on, verbose logging off.
} else {
define( 'SCRIPT_DEBUG', true );
}Hardening constants that belong in every production boilerplate:
define( 'DISALLOW_FILE_EDIT', true );
define( 'FORCE_SSL_ADMIN', true );
define( 'WP_POST_REVISIONS', 5 );
define( 'AUTOMATIC_UPDATER_DISABLED', true ); // when deploys own core updatesUse DISALLOW_FILE_MODS only when the whole tree is immutable and updates land through CI. On managed hosts that still patch plugins in the dashboard, that constant will fight the host.
Authentication keys and salts are not decoration. Rotate them after a compromise; every session ends immediately. Store rotation in your deploy runbook, not in a sticky note.
Linting with @wordpress/scripts
Linting is how you stay compatible with Gutenberg without memorising every ESLint rule. The package @wordpress/scripts wraps webpack, Babel, ESLint, Stylelint, Jest, and Playwright behind familiar npm scripts.
Typical package.json surface for a block plugin:
{
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build",
"lint:js": "wp-scripts lint-js",
"lint:css": "wp-scripts lint-css",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"format": "wp-scripts format",
"packages-update": "wp-scripts packages-update"
}
}Run npm run lint:js in CI on every pull request. Format with Prettier via wp-scripts format so diffs stay about logic, not quote style.
PHP still needs its own gate. Install WordPress Coding Standards (WPCS) through Composer and run PHPCS against themes and plugins. Do not expect @wordpress/scripts to replace that; it owns the JS and CSS half of the stack.
A practical CI order that catches most regressions:
composer phpcs(or your PHPCS wrapper)npm run lint:jsandnpm run lint:cssnpm run buildso a broken webpack config fails before review- Optional:
wp-env startthen plugin PHPUnit or Playwright from the scripts package
Ignore generated build/ output in git reviews unless the project deliberately commits compiled assets for hosts without Node. Prefer building in CI or on deploy. EditorConfig plus the default Prettier config from the scripts package keeps indentation fights out of pull requests.
If a legacy theme still ships jQuery-era admin scripts, introduce @wordpress/scripts for new blocks first rather than rewriting every enqueue on day one. Mixed stacks are fine; mixed rules are not. Pick one ESLint config per package directory.
How to enable WP_DEBUG without showing errors
Never show PHP errors to visitors. Log them.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log by default
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );Better: point WP_DEBUG_LOG at a path outside the document root, and deny HTTP access to any leftover debug.log under wp-content. On production, leave WP_DEBUG false unless you are mid-incident. Turn SAVEQUERIES on only for short profiling sessions; it costs memory and should never stay on overnight.
Complement constants with tools that explain what happened:
- Query Monitor for hooks, HTTP calls, and slow queries in the admin bar
- Xdebug attached to the wp-env PHP container when you need step-through
- Browser DevTools with
SCRIPT_DEBUGso you load unminified core scripts while chasing a conflict
When a white screen hits staging, check debug.log, the host error log, and wp-env logs (if local) before installing another “debug” plugin. Most production outages from misconfiguration show up as a single fatal in the log, not as a missing feature.
Reproduce the bug on a throwaway wp-env instance with the same plugin set before changing production constants. If the issue only appears under object cache or a CDN, note that in the ticket; local Docker will not invent Redis for you unless you add it to the env config.
How to build WordPress blocks with create-block
Interactive blocks are no longer optional side projects. The current default path is:
- Scaffold with
npx @wordpress/create-block my-block --variant dynamic(or static, depending on render strategy) - Develop against wp-env so
block.jsonregistration matches a real admin - Use
npm startfor watch builds andnpm run buildfor production assets - Register the block from PHP with
register_block_type( __DIR__ . '/build' )when metadata lives inblock.json
Keep editor and front-end scripts separate in block.json so you do not ship editor-only React to every visitor. Prefer viewScript / viewStyle for front-end interactivity instead of dumping everything into one bundle.
For themes that still enqueue classic assets, migrate new interactive pieces to blocks incrementally. A hybrid theme can ship block patterns and a few custom blocks while the rest of the templates remain PHP. That is normal in 2026; a full Site Editor rewrite is a product decision, not a tooling requirement.
When dependencies drift, npx wp-scripts packages-update refreshes @wordpress/* packages within the scripts toolchain. Pin major versions in package-lock.json and review the lockfile in PRs the same way you review Composer updates.
Disable emojis and XML-RPC with an mu-plugin
You do not need a “disable everything” plugin from the directory. A small must-use plugin keeps the rules in version control:
<?php
/**
* Plugin Name: Lean core
*/
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
remove_action( 'wp_head', 'wp_generator' );
add_filter( 'xmlrpc_enabled', '__return_false' );Disable XML-RPC only when you are sure no remote clients need it. Disable emoji scripts when you control brand icons and want fewer front-end requests. Leave RSS alone on content sites; brochure sites can shut feeds off with an intentional hook, not a random snippet from a forum.
How to configure Composer 2 for WordPress dependencies and plugins
Composer 2 manages PHP packages, plugins, themes, and developer tooling deterministically across an engineering team. In professional WordPress development, dependencies should never be committed directly to version control as unversioned zip archives downloaded by hand from the plugin repository. Using Composer allows you to declare exact versions, automate security updates in CI pipelines, and keep the git repository clean.
WordPress plugins and themes available on WordPress.org can be installed directly through Composer using the public WPackagist repository (wpackagist.org). The composer/installers package ensures that packages of type wordpress-plugin are placed into wp-content/plugins/ and packages of type wordpress-muplugin are installed into wp-content/mu-plugins/, rather than defaulting to the standard vendor/ directory.
A battle-tested composer.json for a modern WordPress project defines strict PHP platform constraints, configures required installers, and pulls in essential developer dependencies:
{
"name": "wppoland/wordpress-project",
"description": "WordPress developer setup with Composer 2",
"type": "project",
"license": "GPL-2.0-or-later",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
}
],
"require": {
"php": ">=8.2",
"composer/installers": "^2.3"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.11",
"wp-coding-standards/wpcs": "^3.1",
"phpstan/phpstan": "^1.12",
"szepeviktor/phpstan-wordpress": "^1.3",
"roave/security-advisories": "dev-latest"
},
"config": {
"allow-plugins": {
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"optimize-autoloader": true,
"sort-packages": true
},
"scripts": {
"phpcs": "phpcs --standard=WordPress src/",
"phpcbf": "phpcbf --standard=WordPress src/",
"phpstan": "phpstan analyse --level=6 src/"
}
}Always commit composer.lock to git. The lockfile guarantees that every developer on the team and every CI runner executes identical package versions down to the commit hash. On production servers, run composer install --no-dev --optimize-autoloader during deployment. This skips developer-only tools, dumps a classmap autoloader for faster execution, and keeps sensitive analysis packages off the live web server.
Static analysis and PHP coding standards with PHPCS and PHPStan
While @wordpress/scripts handles JavaScript and CSS quality, the server-side codebase requires equal rigor. Running static analysis and code sniffs locally prevents syntax errors, unsafe database queries, unescaped output, and missing security nonces from reaching code review.
Enforcing WordPress Coding Standards with PHPCS
WordPress maintains official coding standards (WPCS) through PHP_CodeSniffer. These rules check for core naming conventions, array formatting, and critical security issues such as unslashed input in $_POST arrays or missing sanitization functions like sanitize_text_field() and esc_html().
Create a phpcs.xml.dist file in your repository root to configure sniffs consistently:
<?xml version="1.0"?>
<ruleset name="WordPress Project Standards">
<description>Custom PHPCS ruleset for WordPress projects.</description>
<file>./src</file>
<arg name="basepath" value="."/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="8"/>
<rule ref="WordPress-Core"/>
<rule ref="WordPress-Docs"/>
<rule ref="WordPress.Security.EscapeOutput"/>
<rule ref="WordPress.Security.ValidatedSanitizedInput"/>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/build/*</exclude-pattern>
</ruleset>Run composer run phpcs locally before opening a pull request. When formatting violations occur, run composer run phpcbf to automatically fix whitespace, indentation, and docblock alignments without touching business logic.
Deep static analysis with PHPStan and phpstan-wordpress
PHPCS checks code structure and formatting, but it does not evaluate type safety or call graph validity. PHPStan inspects PHP code without running it, discovering undefined methods, incorrect parameter types, and dead code branches. Because WordPress relies heavily on dynamic action hooks and global variables, the extension szepeviktor/phpstan-wordpress teaches PHPStan about core function signatures, WP_Post properties, and action hook arguments.
Configure phpstan.neon.dist at rule level 6:
includes:
- vendor/szepeviktor/phpstan-wordpress/extension.neon
parameters:
level: 6
paths:
- src/
excludePaths:
- vendor/
- node_modules/
- build/Running PHPStan in your continuous integration pipeline ensures that refactoring a function signature or renaming a hook callback cannot silently break runtime operations in production.
How to configure Xdebug 3 for Docker and IDE step debugging
Relying solely on var_dump() or log statements slows down troubleshooting when debugging complex filters, REST API endpoints, or database transactions. Xdebug 3 provides step-through debugging, call stack inspection, and variable watch expressions directly inside Visual Studio Code or PhpStorm.
In modern Docker environments like wp-env or custom Docker Compose stacks, Xdebug 3 runs inside the application container while connecting back to your IDE on the host machine. Configure Xdebug using modern mode directives in your PHP configuration:
[xdebug]
zend_extension=xdebug.so
xdebug.mode=debug,develop
xdebug.start_with_request=trigger
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.log=/tmp/xdebug.log
xdebug.discover_client_host=0Setting xdebug.start_with_request=trigger is essential for developer productivity. If Xdebug runs on every incoming HTTP request, page load times increase significantly. With trigger mode enabled, Xdebug activates only when an XDEBUG_SESSION browser cookie or query parameter is present, which you can toggle using browser extensions like Xdebug Helper.
For Visual Studio Code, configure your .vscode/launch.json file with correct path mappings between the host operating system and the Docker container filesystem:
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9003,
"pathMappings": {
"/var/www/html/wp-content/plugins/my-plugin": "${workspaceFolder}"
}
}
]
}Path mappings ensure that breakpoints placed in your local editor match execution lines inside the container. Never deploy Xdebug to a staging or production server; its runtime overhead degrades throughput and exposes internal state.
Advanced wp-config hardening and database constants
The wp-config.php file controls foundational runtime behaviour before WordPress loads core themes or plugins. Beyond standard debugging toggles, seasoned developers configure memory limits, cookie security, cron execution, and database repair flags to harden both development and production environments.
Memory limits and background execution
By default, WordPress allocates 40 MB of PHP memory for single-site front-end requests. High-traffic WooCommerce stores, complex block layouts, and headless REST endpoints quickly exceed this threshold, leading to fatal memory allocation errors during peak traffic. Set explicit memory limits in wp-config.php:
define( WP_MEMORY_LIMIT, 256M );
define( WP_MAX_MEMORY_LIMIT, 512M );The WP_MAX_MEMORY_LIMIT constant applies to admin requests and heavy background tasks such as image resizing, bulk imports, and database migrations.
Decoupling WP-Cron from HTTP visitor traffic
By default, WordPress triggers internal cron tasks during regular visitor page loads. On low-traffic sites, scheduled posts and maintenance jobs miss their deadlines. On high-traffic sites, concurrent incoming requests simultaneously spawn duplicate cron processes, causing database connection spikes.
Disable visitor-driven cron execution in wp-config.php:
define( DISABLE_WP_CRON, true );
define( WP_CRON_LOCK_TIMEOUT, 60 );Then configure a real system crontab on the host server or in a dedicated container to run WP-CLI every five minutes:
*/5 * * * * wp cron event run --due-now --path=/var/www/html > /dev/null 2>&1File system permissions and database repair
When WordPress prompts for FTP credentials during media uploads or updates, developers often mistakenly chmod entire directories to 777. The secure approach is defining the filesystem method explicitly:
define( FS_METHOD, direct );
define( FS_CHMOD_DIR, 0755 );
define( FS_CHMOD_FILE, 0644 );During severe database corruptions, WordPress offers a built-in repair utility accessed at /wp-admin/maint/repair.php. Enable it only during recovery:
define( WP_ALLOW_REPAIR, true );Remove WP_ALLOW_REPAIR immediately after maintenance finishes. Leaving it active allows unauthenticated visitors to trigger database repair and optimization routines.
WordPress site launch checklist for developers
Before a client site goes live:
WP_ENVIRONMENT_TYPEisproduction.DISALLOW_FILE_EDITis true; SSL admin is forced.- Revisions are capped; debug display is off.
- Local and CI both start from the same
.wp-env.jsonand pass@wordpress/scriptslint and build. - Block assets are production builds, not watch-mode leftovers.
- Salts are unique;
debug.logis not web-accessible.
A WordPress developer setup that you can recreate from git is worth more than a clever wp-config.php alone. Local parity, lint gates, and honest logging are what keep block work and classic PHP work from fighting each other.
Need a senior review of an existing stack or a greenfield block plugin? Talk to a WordPress developer at WPPoland.
A developer setup without secrets hygiene still fails a WordPress security audit: local keys, shared .env, and staging clones are the usual holes.







