makertim/multisite-env-switcher

Switch ENV based on request domain

Maintainers

Package info

github.com/MakerTim/multisite-env-switcher

pkg:composer/makertim/multisite-env-switcher

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.1 2026-08-17 21:00 UTC

This package is auto-updated.

Last update: 2026-08-18 17:40:13 UTC


README

PHP from Packagist Latest Version on Packagist Software License

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

https://github.com/vlucas/phpdotenv

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:

  1. The $host argument passed to load()
  2. $_SERVER[<HEADER_FORWARDED_HOST>], when configured (see below)
  3. $_SERVER['HTTP_HOST']
  4. $_SERVER['SERVER_NAME']
  5. $_SERVER['SERVER_ADDR']
  6. "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.