Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Multisite Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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
  1. Register the bundle (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
  1. 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.

  2. Look first at these three seams — everything else hangs off them:

    • the SiteManager / site provider service (how you ask "which site am I on?"),
    • the request listener that resolves host → site and stashes it on the request,
    • the router integration that prefixes generated URLs with the active site's host/path.
  3. 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.

Implementation Patterns

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.

Gotchas and Tips

  • Host matching is literal and fussy. 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.
  • No match is not a soft failure. If nothing resolves, the listener typically leaves the site null and downstream code fatals on ->getContentPath(). Add an explicit guard and a sane 404/redirect for unknown hosts before anything touches the site.
  • CLI and queue workers have no site. There is no request, so no host, so no active site. Commands and message handlers must take a site identifier as an argument and resolve it explicitly — do not assume the listener ran.
  • Reverse proxies and load balancers rewrite 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.
  • Bundle/compiler order matters. The site listener must run before the CMF dynamic router resolves the route from PHPCR; if you also have security or locale listeners that touch routing, priority tuning is where the afternoon goes. Use bin/console debug:event-dispatcher to inspect order.
  • Two caches to clear, not one. Symfony's container cache and the PHPCR/Jackalope node cache. Stale site config after editing a Site document is usually the latter — restart the session or the worker.
  • Overriding the 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 is not magic. The site's locale does not automatically set the request locale or route _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.
  • Version/maintenance risk is the headline gotcha. With ~5 stars, zero dependents and a Symfony 2/3-era CMF dependency graph (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.
  • Extension points worth knowing: a custom site provider (to resolve sites from a DB or API instead of PHPCR), a decoration of the site manager (caching, default-site fallback), and a kernel.request listener registered after site resolution for anything site-dependent (locale, theme, feature flags).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package