liip/theme-bundle
LiipThemeBundle adds flexible theme support to Symfony apps. Define multiple themes with template and asset fallbacks, switch themes per request or environment, and reuse a default theme. It simplifies building branded, multi-theme sites while preserving Symfony’s standard rendering and asset workfl...
First, the framework reality check: liip/theme-bundle is a Symfony bundle, not a Composer package you can drop into a Laravel app. There is no service provider, no facade, no config/liip_theme.php. If you composer require liip/theme-bundle in Laravel you get an inert vendor directory — the Liip\ThemeBundle\LiipThemeBundle class only does anything when a Symfony Kernel registers it. So treat this assessment as "steal the architecture, not the package."
If you're inheriting a Symfony app that uses it:
composer require liip/theme-bundle (last tag is 2019-era, so pin it and expect PHP 7.x / Symfony 2.8–3.4 idioms — XML/YAML service definitions, AppKernel::registerBundles()).app/AppKernel.php and add a liip_theme block to app/config/config.yml:liip_theme:
themes: ['web', 'tablet', 'mobile']
active_theme: 'web'
autodetect_theme: false
cookie:
name: liip_theme
Resources/doc/index.md in the vendor dir, then DependencyInjection/Configuration.php — that file is the authoritative list of config keys for the version you installed, and it's the fastest way to settle "is this key themes or available_themes?" questions.app/Resources/themes/mobile/AcmeBundle/Default/index.html.twig, set active_theme: mobile, and reload. Your controller still returns AcmeBundle:Default:index.html.twig — the bundle swaps the resolved path underneath you. That "logical template name, physical theme path" trick is the entire value proposition.If you're building this in Laravel: don't. You can reproduce 90% of it with View::prependNamespace() / View::addLocation() in a service provider plus a middleware that resolves the active theme. That's roughly 40 lines and no archived dependency.
Directory convention and fallback chain. Templates live under app/Resources/themes/<theme>/<Bundle>/<Controller>/<template>.html.twig. Resolution is: active theme path → default theme path → original bundle path. The fallback is the feature, not a bug — you only override the 5 templates that actually differ, and everything else keeps rendering from the bundle. In practice you build a theme by copying one template, not the whole tree.
Runtime theme switching via the service. Inject liip_theme.active_theme (the ActiveTheme service) and call setActiveTheme('mobile') before rendering. This is how you do per-user themes after authentication:
public function dashboardAction()
{
$this->get('liip_theme.active_theme')->setActiveTheme($user->getPreferredTheme());
return ['template' => 'AcmeBundle:Dashboard:index.html.twig'];
}
Order matters: the bundle's request listener sets the theme early (from cookie/autodetect), so anything you set in a controller overrides it for that request only.
Autodetection service. autodetect_theme accepts a service id. Implement a small class that returns a theme name from the request (User-Agent, host, subdomain, Accept-Language) and register it. Keep it pure and fast — it runs on every request via a kernel.request listener. Persist the result in the cookie rather than re-deriving it everywhere.
Cookie persistence + a switch endpoint. The bundle's cookie config lets a /theme/{theme} route set the preference and redirect back. Guard it: validate the submitted theme against themes before persisting, or you'll inject an arbitrary path segment into the template locator.
Twig stays dumb. Because {% extends %} and {% include %} use logical bundle names, adding a fourth theme requires zero template edits — you only add files under the new theme directory. Resist the urge to write {% include 'themes/' ~ liip_theme.activeTheme ~ '/...' %}; that defeats the locator and breaks fallback.
Assets are not the bundle's problem. LiipThemeBundle only touches templates. Theme-specific CSS/JS needs Assetic/webpack output dirs keyed per theme, or use the active-theme value exposed to Twig (check the Twig extension in your installed version) to build the path.
The Laravel translation of the pattern:
// ThemeServiceProvider::boot()
View::prependNamespace('theme', resource_path('themes/'.app(Theme::class)->active()));
// then view('theme::dashboard.index') falls back to view('dashboard.index')
Pair that with a ResolveTheme middleware (cookie → user pref → header → default), a container-scoped Theme singleton, and a theme() helper. That is structurally identical to ActiveTheme + ThemeRequestListener + the file locator, and it's what I'd actually ship.
active_theme must appear in themes, or you get an exception at container compile time, not at request time. Same for a theme name set at runtime — validate before setActiveTheme().themes/Mobile vs themes/mobile) doesn't error — you just silently get the default theme's template and wonder why your override never applies. Confirm the resolved path with the Symfony profiler / Twig debug, or grep the compiled template cache. Add a temporary {{ dump() }} of the active theme value.themes, or editing the locator config all require cache:clear. The template locator and Twig's compiled cache both need to be rebuilt; a stale cache looks exactly like "the bundle is broken."Vary: Cookie, or key the cache on the theme cookie explicitly.tokenStorage, you'll get null. Prefer cookie/header detection at request time and override from the controller once the user is known.liip_theme.active_theme, or point autodetect_theme at your own service. All three are plain Symfony service overrides — no bundle surgery needed, which is why the bundle survived as long as it did.symfony/ux-era templating; on Laravel, a service provider + middleware + View::prependNamespace() gives you the same override-with-fallback semantics in an afternoon, with no archived dependency and no framework mismatch.How can I help you explore Laravel packages today?