makertim / multisite-env-switcher
Switch ENV based on request domain
Package info
github.com/MakerTim/multisite-env-switcher
pkg:composer/makertim/multisite-env-switcher
Requires
- php: ^8.0
Requires (Dev)
- phpmd/phpmd: 2.*
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^4.0
README
This PHP library helps you to use one deployment / instance / codebase for multiple sites, e.g. live database switching.
It resolves the active site from the request host, then maps that site's prefixed variables to their normal names.
Resolved values are written to getenv(), $_ENV, and $_SERVER. Requests without a matching host, including CLI processes, fall back to the DEFAULT site.
For example, when ALPHA is active, SITE_ALPHA_DB_NAME is exposed as DB_NAME.
Installation
Install the package with Composer:
composer require makertim/multisite-env-switcher
And ensure it's loaded before you boot a kernel / use environment variables.
Usage
On top of the entrypoint (like index.php or other autoload/included script) include
\MakerTim\MultisiteEnvSwitcher\MultisiteEnvSwitcher::load();
Call
MultisiteEnvSwitcher::load()before you use / validate environment variables. When using external methods of loading env, you should load this after them.
Composer autoload
Add a bootstrap file, e.g. autoload.env.php to Composer's autoload configuration:
composer.json
{
...
"autoload": {
"files": [
"src/autoload.env.php"
]
},
...
}
src/autoload.env.php
<?php ... \MakerTim\MultisiteEnvSwitcher\MultisiteEnvSwitcher::load(); ...
Dotenv example
src/autoload.env.php
$dotenv = \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__)); $dotenv->load(); \MakerTim\MultisiteEnvSwitcher\MultisiteEnvSwitcher::load(); $dotenv->required([ 'APP_DOMAIN', 'APP_URL', 'APP_SECRET', 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', ])->notEmpty(); }
Configure sites
Set APP_SITES to a space-separated list of site names.
Define an APP_SITE_DOMAIN__<SITE> regular expression for each site you want to load.
The regular expression is anchored on both ends (by using ~^...$~), so make sure the expression is a full match.
Notice: hosts are lowercased before matching.
Example
APP_SITES="ALPHA BETA" APP_SITE_DOMAIN__ALPHA='(www\.)?alpha\.(test|example\.com)' APP_SITE_DOMAIN__BETA='beta\.example\.com'
Sites are checked in the order listed in APP_SITES.
The first match wins, so put broad patterns such as .*\.example\.com last.
Common pitfall, when using a dotenv file/shell evaluated method is escaping the
\Single quote don't need\escaping, but double-qouted values do.APP_SITE_DOMAIN__ALPHA='alpha\.example\.com' APP_SITE_DOMAIN__ALPHA="alpha\\.example\\.com"
For compatability with the abandoned https://github.com/drunomics/multisite-request-matcher library there is backwards support for uppercase or lowercase site suffixes.
E.g. a site named ALPHA, it is supported to use APP_SITE_DOMAIN__ALPHA and APP_SITE_DOMAIN__alpha.
Best practices recommend uppercased environment values.
Override values per site
Prefix site-specific variables with SITE_<SITE>_. When the site is resolved, the prefix is removed and the value is published under its normal name:
# Shared by every site DB_HOST=db APP_SECRET=s3cr3t # Configure sites APP_SITES="alpha beta" APP_SITE_DOMAIN__ALPHA="alpha\.example\.com" APP_SITE_DOMAIN__BETA="beta\.example\.com" # Per site SITE_ALPHA_DB_NAME=alpha_db SITE_ALPHA_DB_USER=alpha_user SITE_ALPHA_MAIL_FROM=alpha@example.com SITE_BETA_DB_NAME=beta_db SITE_BETA_DB_USER=beta_user SITE_BETA_MAIL_FROM=beta@example.com
A request to alpha.example.com receives DB_NAME=alpha_db.
A request to beta.example.com receives DB_NAME=beta_db.
Variables without a SITE_ prefix are left unchanged unless the active site provides an override.
So other requests do have access to the other variables, since SITE_... are not removed after requests.
Still overrides for other sites are not published, so an ALPHA request does not receive values from SITE_BETA_*.
The APP_SITES variable always uppercase the ENV matcher. In the example it contains alpha but the corresponding variables still use SITE_ALPHA_*.
Values are copied as-is, including empty strings and special characters. They do not get evaluated when they are copied. So everything is kept as-is.
Default site
If no domain pattern matches, the switcher uses the DEFAULT site. This also applies to CLI processes when no host is available.
SITE_DEFAULT_DB_NAME=default_db SITE_DEFAULT_MAIL_FROM=noreply@example.com
DEFAULT does not need to be included in APP_SITES and does not have a domain pattern.
If APP_SITES is not set, every request resolves to DEFAULT.
It has the same behaviour as not prefixing it with SITE_DEFAULT_ but it ensures that it can never leak to the other domains when there is an issue.
Force a site
It is possible to force a site to be current. E.g. when using the cli, cron or something.
When the SITE environment value is set it skips the domain matching process and apply the site override.
It is supported to use the load() multiple times at runtime.
When the SITE env is changed, it can load a different site.
Creating a loop that does something for each APP_SITES entry, set SITE=... and MultisiteEnvSwitcher::load
WARNING: it does not unload the previous site overwritten values!
SITE=BETA ./bin/console cache:clear SITE=BETA ./artisan list SITE=BETA ./drush st
Load by host name
You can also pass a host directly to load().
MultisiteEnvSwitcher::load('beta.example.com');
After load() completes, SITE contains the resolved site (or default when no match is found)
$site = getenv('SITE');
Request / Host load order
If the configured header is not present, the switcher continues through the normal host-resolution chain.
Only use a forwarded host header when it is set by a trusted proxy.
A client-controlled header could allow a request to select another site's environment variables.
Resolving the hosts is in the order:
- The
$hostargument passed toload() $_SERVER[<HEADER_FORWARDED_HOST>], when configured (see below)$_SERVER['HTTP_HOST']$_SERVER['SERVER_NAME']$_SERVER['SERVER_ADDR']"DEFAULT"(fallback)
Custom host-header support / Proxy and load balancer support
If the HEADER_FORWARDED_HOST env is filled, it uses it's value to get from the $_SERVER pool.
Most often, behind a Proxy it is something like HTTP_X_FORWARDED_HOST, then you need:
HEADER_FORWARDED_HOST=HTTP_X_FORWARDED_HOST
Configuration reference
| Variable | Description |
|---|---|
APP_SITES |
Space-separated list of site names. Defaults to DEFAULT. |
APP_SITE_DOMAIN__<SITE> |
Anchored regular expression matched against the lowercased request host. |
SITE_<SITE>_<NAME> |
Site-specific value published as <NAME> when <SITE> is active. |
SITE |
Forces a site and skips domain matching. Updated with the resolved site after load(). |
HEADER_FORWARDED_HOST |
Name of the $_SERVER key containing the forwarded host, for example HTTP_X_FORWARDED_HOST. |
Testing & Development
Run the test suite directly:
composer run test
Run static analysis and coding-standard checks with:
composer check # lint + analyze composer format # apply coding standard fixes
Contributing
Pull requests are welcome.
License
Licensed under the MIT License. See LICENSE.