roots/wp-config
Roots WP Config provides a clean, code-first way to define WordPress constants and environment-specific settings. It integrates with Bedrock and Dotenv, letting you manage wp-config values safely and consistently across environments.
Roots wp-config is the fluent, class-based config layer that Bedrock (and by extension the Sage stack) uses in place of the procedural top of wp-config.php. It collapses all the define() calls, getenv() reads, and if (defined(...)) guards into a tiny static API:
Config::define($name, $value) — register a constantConfig::get($name) — read a registered valueConfig::set($name, $value) — update the value without redefiningConfig::apply() — materialize queued constants via PHP define()Install
composer require roots/wp-config
You'll almost always pair it with vlucas/phpdotenv and Roots' oscarotero/env wrapper (env()), which gives you Laravel-style type coercion. On Bedrock it's already wired up — don't install anything.
First use case — replace your wp-config.php
wp-config.php a thin bootstrap: require __DIR__ . '/vendor/autoload.php'..env if present, then Env::init().WP_ENV with plain define() so later code can branch on it.Config::define() every WordPress constant, deriving values from Config::get() where needed.require an environment file: config/environments/{WP_ENV}.php.Config::apply() at the bottom, then require_once ABSPATH . 'wp-settings.php'.Minimal shape:
<?php
use Roots\WPConfig\Config;
require __DIR__ . '/vendor/autoload.php';
if (file_exists(__DIR__ . '/.env')) {
Dotenv\Dotenv::createUnsafeImmutable(__DIR__)->load();
}
Env::init();
define('WP_ENV', env('WP_ENV') ?: 'production');
Config::define('DB_NAME', env('DB_NAME'));
Config::define('DB_HOST', env('DB_HOST') ?: 'localhost');
Config::define('WP_HOME', env('WP_HOME'));
Config::define('CONTENT_DIR', '/app');
Config::define('WP_CONTENT_DIR', __DIR__ . Config::get('CONTENT_DIR'));
Config::define('WP_CONTENT_URL', Config::get('WP_HOME') . Config::get('CONTENT_DIR'));
$table_prefix = env('DB_PREFIX') ?: 'wp_';
require __DIR__ . '/config/environments/' . WP_ENV . '.php';
Config::apply();
if (!defined('ABSPATH')) {
define('ABSPATH', __DIR__ . '/wp/');
}
require_once ABSPATH . 'wp-settings.php';
Where to look first: the Bedrock repo's wp-config.php and config/environments/*.php are the canonical reference. The package README is intentionally terse — copy Bedrock's patterns rather than inventing your own.
1. Layered config (base + environment). Keep wp-config.php for constants that never change (charset, collation, content dir, salts location) and one file per environment (development.php, staging.php, production.php, plus a gitignored local.php). WP_ENV picks which to require. This mirrors Laravel's per-environment config and keeps the diff surface small.
2. .env-driven values. Read everything sensitive from .env via env(). oscarotero/env returns real booleans/ints/null, so WP_DEBUG=false actually works. Commit .env.example; never commit .env.
3. Derived values through Config::get(). Because define() registers values before they are applied, you can compose them:
Config::define('WP_CONTENT_URL', Config::get('WP_HOME') . Config::get('CONTENT_DIR'));
This is the main reason to route every constant through Config rather than raw define().
4. Config::set() for late overrides. Use it when local.php or a guard clause needs to adjust a queued value (WP_DEBUG, WP_HOME) without redefining it. The final value is what apply() defines.
5. Environment files with guard clauses. A typical config/environments/development.php:
<?php
use Roots\WPConfig\Config;
Config::define('WP_DEBUG', true);
Config::define('WP_DEBUG_LOG', true);
Config::define('WP_DEBUG_DISPLAY', false);
Config::define('SCRIPT_DEBUG', true);
Config::define('DISALLOW_INDEXING', true);
if (env('WP_MULTISITE')) {
Config::define('WP_ALLOW_MULTISITE', true);
}
6. Local overrides. Require a gitignored local.php last (before apply()) so developers can set their own WP_HOME (e.g. https://myproject.test) without touching shared files.
7. Salts. Keep the eight *_KEY / *_SALT constants routed through Config::define() so they can rotate per environment. Generate with wp config shuffle-salts into .env, or copy from the WordPress salt API into your .env.example.
8. Plain define() for WordPress-mandated values. ABSPATH and $table_prefix should stay as plain PHP — they're needed before the Config flow, or they're variables rather than constants. WP_ENV itself is often plain too, so early code can branch before Config loads.
9. WP-CLI aware. wp-config.php runs on every CLI invocation. Guard expensive work (HTTP, DB) behind if (defined('WP_CLI') && WP_CLI) { ... }.
10. Multisite. Enable WP_ALLOW_MULTISITE first, then add the full MULTISITE / SUBDOMAIN_INSTALL / DOMAIN_CURRENT_SITE / PATH_CURRENT_SITE / SITE_ID_CURRENT_SITE / BLOG_ID_CURRENT_SITE set in one block inside the environment file.
apply() must be called, and last. Forget it and no constants exist — WordPress falls back to defaults or dies on the DB. Call it too early and subsequent Config::define() calls are silently ignored.Config::get() reads the Config store, not arbitrary PHP constants. define('FOO', 'bar'); Config::get('FOO') returns null. Route anything you want to read back through Config::define() / Config::set().apply() (and define() with the standard guard) skips names PHP has already defined. A stray define('WP_DEBUG', true) earlier in the bootstrap silently defeats your environment file. grep -R "define(" . is your first debug step..env values are strings by default. Use env() (oscarotero/env) rather than raw getenv(), or cast explicitly: env('WP_DEBUG') === 'true'.WP_CONTENT_URL depends on WP_HOME and CONTENT_DIR; custom UPLOADS depends on both. Define bases first, then derive.$table_prefix is not a constant. It must remain the PHP variable WordPress reads — do not try Config::define('table_prefix', ...).WP_ALLOW_MULTISITE before MULTISITE, and the DOMAIN_CURRENT_SITE / PATH_CURRENT_SITE / SITE_ID_CURRENT_SITE / BLOG_ID_CURRENT_SITE set must be complete together. Split across files and you get redirect loops.var_dump(Config::get('DB_NAME')) before apply(), then defined('DB_NAME') ? constant('DB_NAME') : 'unset' after, to confirm the constants actually landed.config(). If you want Config::get('app.host')-style access, write your own wrapper.function env_or($k, $d) { return env($k) ?: $d; }) or a thin subclass of Config for WP_HOME-derived URLs pays for itself quickly. The class is small and stable — wrap, don't patch..env, config/environments/*.php, and config/application.php. Do not edit wp-config.php — it explicitly warns you not to.How can I help you explore Laravel packages today?