valiton/multisite-bundle
Symfony bundle for running multiple sites from one codebase. Configure distinct hosts, locales and settings per site, with automatic host-based detection and shared routing, templates and services across all your applications.
First, calibrate expectations: this is not a Laravel package. valiton/multisite-bundle is a Symfony CMF bundle that sits on top of symfony-cmf/routing-bundle, Doctrine PHPCR-ODM and Jackalope. There is no service provider, no facade, no config/multisite.php. You are adopting a Symfony bundle into a Symfony app — if you are in a Laravel codebase, the realistic move is to port the concept (host → site → content root mapping) rather than the code.
Mental mapping for a Laravel dev:
| Laravel | This bundle |
|---|---|
| Service provider bindings | Symfony DI aliases/compiler passes |
| Middleware | Kernel request listener (site resolution) |
| Eloquent models | PHPCR-ODM documents |
| Blade | Twig |
config/*.php |
config/packages/valiton_multisite.yaml (YAML) |
Minimal path to first render:
composer require valiton/multisite-bundle symfony-cmf/routing-bundle doctrine/phpcr-odm
config/bundles.php for Symfony 4+, AppKernel::registerBundles() on legacy projects) and enable it in YAML:# config/packages/valiton_multisite.yaml
valiton_multisite:
site_document_class: App\Document\Site # subclass of the bundle's Site document
Create Site documents under your PHPCR content root (via fixtures, admin UI, or a console command). Each site carries at minimum a host list, a content path (the PHPCR node that becomes that site's root), and usually a locale.
Look first at these three seams — everything else hangs off them:
SiteManager / site provider service (how you ask "which site am I on?"),First use case: one codebase, one content repository, two hostnames (example.com, example.de) rendering different content trees and different locales. Seed two sites, curl -H "Host: example.de" http://localhost/ and confirm the listener picks the German site.
Site resolution is request-scoped — inject, don't re-derive. The bundle's site provider is the single source of truth for "current site". Inject it into controllers/services and read the active site rather than re-parsing $request->getHost() yourself; otherwise you duplicate (and drift from) the bundle's host-matching and fallback rules.
public function __construct(private SiteManager $siteManager) {}
public function show(): Response
{
$site = $this->siteManager->getSite(); // null on CLI / unmatched host
$root = $site->getContentPath(); // e.g. /cms/sites/de
// load documents relative to $root
}
Treat the site's content path as a tenancy boundary. Every document lookup, menu build and search query should be rooted at the site's path, not at /cms. This is the pattern that makes multisite actually multisite rather than "shared content with a different header".
Route generation is site-aware — lean on the router rather than hand-building URLs. Use the CMF router (or a site-aware wrapper) so generated links carry the correct host/path for the target site, not the current one. Cross-site links (e.g. a language switcher between .com and .de) are the main place hand-rolled url() calls break under a reverse proxy.
Per-site Twig context. Expose the active site to templates (a small Twig extension or a global cmf_site) and drive theme/layout/navigation from it. Template overrides per site (@App/site_de/base.html.twig) keep marketing variants out of if ($site->getName() === 'de') spaghetti.
Functional tests must set Host. WebTestCase calls hit localhost by default, so the site listener resolves nothing. Set the server parameter explicitly:
$client->request('GET', '/', [], [], ['HTTP_HOST' => 'example.de']);
Fixtures/config are the deployment surface. Because sites live in the content repository, onboarding a new domain is a data operation, not a code deploy. Wrap that in an idempotent console command so staging/prod provisioning is repeatable rather than a hand-typed PHPCR node.
www.example.com, example.com, example.com:8080 and leading/trailing-slash variants are different strings. Configure every hostname (including www) explicitly per site, and test with a port — the classic "works locally, 404s in prod" failure.null and downstream code fatals on ->getContentPath(). Add an explicit guard and a sane 404/redirect for unknown hosts before anything touches the site.Host. Behind an ALB/nginx, ensure the proxied host is forwarded (TrustedProxies, proxy_set_header Host $host) or every request resolves to the default site.bin/console debug:event-dispatcher to inspect order.Site document is usually the latter — restart the session or the worker.Site document is a config-time decision. Point site_document_class at your subclass and keep the PHPCR node types in sync; swapping the class after content exists means a repository migration, not a composer update._locale. Wire that yourself (a small listener after site resolution), or you will get correct content with wrong translations and date filters in the wrong language.symfony-cmf/routing-bundle, PHPCR-ODM, Jackalope), expect PHP/Symfony version friction on modern stacks and no upstream fixes. Vendor the concept or pin hard; do not plan to upgrade Symfony majors around it.kernel.request listener registered after site resolution for anything site-dependent (locale, theme, feature flags).How can I help you explore Laravel packages today?