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

Theming Bundle Laravel Package

sylius/theming-bundle

Sylius Theming Bundle adds flexible theme support to Sylius storefronts, letting you organize templates, assets, and configuration into reusable themes with inheritance and per-channel theme selection.

View on GitHub
Deep Wiki
Context7

Getting Started

Start by confirming you cannot install it. sylius/theming-bundle is a Symfony bundle built for Sylius — it registers a Bundle class, a DI extension, Twig loaders, and Doctrine-backed theme entities. It is also archived with zero dependents. composer require sylius/theming-bundle in a Laravel app will not produce a service provider; it will either fail resolution against your Symfony polyfill/console versions or install dead code. Treat the repository as a reference design, not a dependency.

Read these files first, in this order:

  1. composer.json — the require block tells you the PHP/Symfony/Sylius era the code targets. If you see symfony/framework-bundle: ~2.x or sylius/*: 0.x, you are reading legacy code and should not expect the API to resemble anything current.
  2. DependencyInjection/Configuration.php and SyliusThemingExtension.php — this is the themes: config tree. It is the most portable idea in the bundle: theme name → filesystem path → optional parent.
  3. The template locator / Twig loader decoration — the class that intercepts a logical template name (@SyliusShop/Product/index.html.twig), rewrites it against the active theme, and falls back to the parent chain. This is the whole value proposition in one class.
  4. Resources/config/services.xml — how the locator is decorated onto the container and at what priority. In Laravel the analogue is where you register a custom ViewFinder in your ThemeServiceProvider.

First use case to model (do not port code — port the shape):

// config/themes.php
return [
    'default' => [
        'path'   => resource_path('themes/default'),
        'parent' => null,
    ],
    'acme' => [
        'path'   => resource_path('themes/acme'),
        'parent' => 'default',
    ],
];

You want a theme name in config, a parent pointer, and a resolver that walks acme → default when a view is missing. Build that with view()->addNamespace() plus a small custom ViewFinder before you write anything else — it is roughly 150 lines and replaces the bundle entirely.

Your first deliverable should be a single feature test asserting that a template present only in the parent renders when the child theme is active. That test is the contract the rest of the system depends on.

Implementation Patterns

Theme resolver contract, bound in a service provider. Keep selection behind an interface so the rest of the app never touches theme strings directly:

interface ThemeResolver
{
    public function resolve(): string;      // active theme key
    public function chain(): array;         // ['acme', 'default']
}

Bind a default implementation reading config('themes.active') in ThemeServiceProvider::register(). Swap it later for a tenant- or host-based resolver without touching controllers or Blade.

Per-request theme binding via middleware. Theme selection is a request-time concern. Resolve it in middleware before the controller, and stash the resolved chain on the container or a small singleton so the finder and asset helper read the same value:

public function handle($request, Closure $next)
{
    $theme = $this->resolver->resolveForHost($request->getHost());
    app(ThemeContext::class)->set($theme, $this->resolver->chain($theme));

    $response = $next($request);
    return $response->header('X-Theme', $theme); // cheap observability
}

Register it in the web group, before anything that composes views (including the exception handler, if you theme error pages).

Fallback chains through a custom ViewFinder. Laravel resolves views through Illuminate\View\FileViewFinder. Extending it with a chain walk gives you inheritance without a second framework:

class ThemedViewFinder extends FileViewFinder
{
    public function find($view)
    {
        foreach (app(ThemeContext::class)->chain() as $theme) {
            try {
                return $this->findIn($theme, $view);
            } catch (ViewNotFoundException) {
                // continue to parent theme
            }
        }
        return parent::find($view);
    }
}

Register it in a deferred provider once the view factory is booted, and mirror the same chain logic for @theme namespaces via view()->addNamespace("theme.{$name}", ...) if you prefer namespaced overrides over finder interception — the namespace approach is less invasive and easier to reason about.

Assets: compile-time, not resolve-time. The bundle predates Vite and assumes asset resolution at runtime. Do not copy that. Themes should each own a Vite entry point and manifest, built per theme, with the active theme's manifest path selected at boot:

// in a ThemeAssets service
public function manifest(string $theme): string
{
    return public_path("build/{$theme}/manifest.json");
}

If the theme is chosen at runtime and the assets are built at deploy time, you must ensure every theme's manifest ships with the deploy. This is the single most common production break in themed Laravel apps.

Per-theme settings. The bundle's themes: config supports arbitrary per-theme keys. Reproduce that with a plain config array and a typed accessor (ThemeConfig::get('colors.primary')) that merges the child theme's array over the parent's recursively. Keep it to presentation values — logos, CSS entry, layout name — not business configuration.

Testing pattern. Write a WithTheme test trait that sets ThemeContext before the request and asserts the resolved chain. One test per override surface (layout, partial, mail template, asset manifest) is enough to catch regressions; do not attempt exhaustive matrix testing across themes.

Gotchas and Tips

Silent fallback is the killer bug. A missing child-theme template falls back to the parent by design — which means a typo in a theme path renders the default brand and passes smoke tests. Assert the resolved theme (the X-Theme header above is the cheapest fix) and add a build-time check that lists every template in the child theme and warns if it is identical to the parent's, which usually indicates an unused or misdirected override.

Inheritance cycles. a → b → a is trivially expressible in a config array and will hang or blow the stack in the finder. Validate the chain on boot with a visited-set walk and throw a clear exception. The original bundle does not protect you here; do it yourself.

Cache keys must include the theme. Laravel's view cache is keyed by view name and path, but any cache you add (rendered fragments, compiled theme config, navigation) must include the theme key or you will serve one brand's markup to another under load. This is the subtlest multi-theme failure and only shows up when you have traffic.

Never let the theme key come from raw user input. If you resolve a theme from a subdomain, query string, or cookie, validate it against the configured theme list before it reaches a filesystem path. A theme name interpolated into resource_path("themes/{$theme}") is a path traversal vector, and the archived bundle offers no protection worth relying on.

Config caching will bite you. Anything you read at runtime via env() inside a theme resolver returns null under config:cache. Put theme selection in config/themes.php, not in service code, and re-run php artisan config:cache in the deploy script after any theme-config change.

Asset manifest staleness is the #1 incident. Theme switched on, manifest not built for it, 500s or unstyled pages. Gate deploys on a check that every configured theme has a public/build/{theme}/manifest.json, and fail the build rather than the request.

Ordering of service provider registration matters. A ViewFinder swap registered after the view service is resolved (e.g. inside a middleware constructor) silently does nothing. Register in register() with $this->app->extend('view.finder', ...) or in boot() before the first view resolves — and write the feature test above so a regression in ordering fails CI.

Debugging: make the resolution visible. Add an artisan theme:resolve {view} command that prints the chain and which file each step would hit. You will run it dozens of times; without it you are dd()-ing inside the finder.

Extension points to steal conceptually — the config tree shape, the locator decoration, and the parent-pointer inheritance model. Everything else (Twig loader, Doctrine theme entity, Sylius channel integration) is framework-specific and should be dropped.

On maintenance: the repository is archived. There is no upstream to merge security fixes from and no compatibility guarantee against current Laravel or Symfony. If someone later asks why the app has a bespoke theme layer, the answer is that this bundle's capability was reimplemented in first-party code on Laravel's view and asset primitives — record that decision in the repo so the choice is not relitigated.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource