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

Cms Themes Laravel Package

yajra/cms-themes

Laravel package for managing CMS themes with theme discovery, activation, Blade view and asset publishing, and a simple API for registering theme paths, helping developers build customizable content-driven sites with swappable front-end themes.

View on GitHub
Deep Wiki
Context7

Getting Started

Gate check before you write a line of app code. The package is a module extracted from YajraCMS and targets the Laravel 5.x era, so the illuminate/* ceiling is the first thing that can kill the idea:

composer show yajra/cms-themes        # PHP + illuminate/* constraints
composer why yajra/cms-themes         # does it drag in yajra/cms core?

If the ceiling is below your framework version, stop here — a config-driven view namespace plus an asset path is ~200 lines and lower risk than a compatibility backport.

Install, then fork. Do not consume it as a top-level dependency. Pull it into a private fork ("repositories": [{"type": "vcs", ...}] or a path repo) so you can pin, patch, and strip CMS-specific coupling. Then:

php artisan vendor:publish --provider="Yajra\Cms\Themes\ThemesServiceProvider"
# confirm exact tags from the provider, typically:
php artisan vendor:publish --tag=config
php artisan vendor:publish --tag=assets

Where to look first, in this order:

  1. composer.json → autoload.files. Any helpers.php here is your global-collision risk and tells you how the theme is addressed in Blade.
  2. The service provider (register/boot) — this is where view paths are prepended, config is merged, and (watch for it) where the filesystem or DB may be read at boot.
  3. The config file — theme root directory, active theme key, asset path, fallback theme.
  4. The resolver class — the actual contract: how a theme slug maps to a view path and an asset path.
  5. Any upload/extraction controller or command. That is the security review, not a feature tour.

First use case — prove precedence in ten minutes. Create resources/views/themes/{slug}/ with one view, point the config's active-theme key at it, and assert the theme's view wins over the app default:

// routes/web.php – throwaway smoke route
Route::get('/_theme-check', function () {
    return view('home', ['marker' => 'theme-active']);
});
// tests/Feature/ThemeSmokeTest.php
public function test_active_theme_view_overrides_default(): void
{
    config(['cms-themes.active' => 'dark']);

    $this->get('/')->assertOk()->assertSee('dark-theme-marker');
}

public function test_missing_theme_falls_back(): void
{
    config(['cms-themes.active' => 'does-not-exist']);

    $this->get('/')->assertOk()->assertSee('default-theme-marker');
}

That second test is the one that matters. A theming layer that 500s on a bad slug is a production incident waiting for a typo in an admin form.

Also verify the resolution happens once per request, not per view:

$this->assertSame(1, DB::connection()->getQueryLog() ? count(DB::getQueryLog()) : 0);
// or simply: no glob()/scandir() in the resolver on the render path

Implementation Patterns

Wrap the vendor classes behind your own interface — day one, not later. Every downstream pattern below assumes this. Application code never imports Yajra\...; it imports App\Themes\ThemeResolver.

namespace App\Themes;

interface ThemeResolver
{
    public function active(): string;
    public function activate(string $slug): void;
    public function available(): array;
    public function asset(string $path): string;
}

Bind the adapter in a service provider. If the fork dies, you change one class.

Resolve the theme at boot or in middleware, never in a Blade template. The single most common mistake is @if(theme() === 'dark') scattered across layouts — untestable and uncacheable. Instead, resolve in middleware and expose it to views as a shared variable:

public function handle($request, Closure $next)
{
    $slug = $this->resolveForRequest($request);   // tenant, cookie, config, A/B bucket
    $this->themes->activate($slug);

    view()->share('theme', $this->themes->active());
    view()->share('themeAsset', fn (string $p) => $this->themes->asset($p));

    return $next($request);
}

Layout selection via config, not hardcoded @extends. Themes ship different layout names. Expose the layout name through the resolver and use a single indirection:

{{-- resources/views/layouts/app.blade.php (the app default) --}}
@extends(config('cms-themes.layouts.app', 'layouts.base'))

Better: have the theme's own root view extend theme::layouts.app, and let the view finder handle precedence. Whichever you choose, pick one and document it in the theme contract.

View precedence — understand the mechanism you are relying on. These packages generally prepend a path to the finder so the theme's views shadow app views, with the app's resources/views remaining as fallback:

View::prependLocation(resource_path("views/themes/{$slug}"));
// or, if namespaced:
view()->getFinder()->prependNamespace('theme', resource_path("views/themes/{$slug}"));

Two consequences to design around:

  • If it is a raw prependLocation, every view name resolves against the theme first — so a theme folder that happens to contain errors/404.blade.php silently takes over error pages. Audit theme folders for accidental shadowing.
  • If it is a namespace (theme::home), you must opt in per view. More explicit, less magic, easier to reason about in code review. Prefer the namespace form when you fork.

Inspect what is actually registered rather than guessing:

dump(View::getFinder()->getPaths());   // global locations, order matters
dump(View::getFinder()->getHints());   // namespaces

Asset handling: publish at deploy, serve by path. Resolve the asset URL from the active theme; never copy files on a web request.

public function asset(string $path): string
{
    $slug = $this->active();

    return asset("themes/{$slug}/{$path}") . '?v=' . config("cms-themes.versions.{$slug}", '1');
}

Ship a deploy step (php artisan theme:publish or a wrapper around vendor:publish/File::copyDirectory) that mirrors resources/views/themes/{slug}/public/ into public/themes/{slug}/. In a containerized environment this must run in the build image, not at runtime, or every replica drifts.

Per-tenant / per-request switching. Keep the activate/reset pair symmetrical so long-running workers (Octane, queue) do not leak state:

$previous = $this->themes->active();
$this->themes->activate($tenant->theme_slug);

try {
    return $next($request);
} finally {
    $this->themes->activate($previous);   // critical under Octane
}

Cache the discovered theme list. available() almost certainly walks directories. Cache it and bust on deploy:

public function available(): array
{
    return Cache::rememberForever('themes.available', fn () => $this->discover());
}
// deploy: php artisan cache:clear  (or forget just this key)

Test the contract, not the implementation. Your feature tests should assert: active theme renders, fallback renders on a bad slug, assets resolve to the right URL, and switching mid-request does not leak. Do that and the fork's internals can change freely.

Gotchas and Tips

The Illuminate ceiling is binary, not negotiable. If the package's illuminate/support constraint caps it below your framework version, you are not "adapting" it — you are backporting it. Price the in-house resolver against that.

config:cache is the classic silent killer. If the service provider reads the filesystem or database during boot() to figure out the active theme, php artisan config:cache and route:cache will either bake in the wrong theme or throw at boot. Test all three cache commands in CI, not just locally.

No fallback is the highest-frequency production failure. A deleted theme folder, a renamed slug, or a botched deploy produces a hard 500 on every page. Enforce, in the resolver:

public function activate(string $slug): void
{
    $safe = array_key_exists($slug, $this->available())
        ? $slug
        : config('cms-themes.fallback', 'default');

    if (! is_dir(resource_path("views/themes/{$safe}"))) {
        $safe = config('cms-themes.fallback', 'default');
    }

    $this->active = $safe;
}

Log the substitution at warning — silent fallbacks are almost as bad as 500s because white-label customers see the wrong branding and nobody notices.

Asset publishing will overwrite your app's public/ files. Publish into a namespaced directory (public/themes/{slug}/), never public/ root, and never publish with --force in a deploy script.

Any upload or zip-extraction path is a stop sign. If the fork contains one, disable the route before it ever reaches a shared environment and review it for zip-slip (../ in entry names), symlink extraction, and PHP files landing under public/. Theme archives from a CMS admin panel are a code-execution surface, full stop. Most teams should delete the feature and accept manual theme deploys.

Global helper collisions. Check autoload.files in the fork's composer.json. A function named theme() or asset_path() collides with other packages and with your own helpers, and the failure is a fatal redeclare at boot — obvious, but only after you have already deployed. Rename to your namespace when you fork.

Per-request filesystem scans cost more than they look. A glob() across theme directories on every request is invisible on a dev laptop and expensive at load. Resolve once, cache the result, and assert it in a test if you can.

view:clear is your debugging reflex. Blade compiles to storage/framework/views keyed by path. When you move theme folders around during development and views appear stale or refuse to update, php artisan view:clear resolves a surprising number of "the override isn't working" reports.

Debug precedence empirically, not from memory. Two lines answer nearly every "why is the wrong template rendering?" question:

dump(View::getFinder()->getPaths());
dump(view()->exists('home'));           // true/false tells you which layer won

Document the theme contract as a deliverable. The manifest fields, the required directory layout (layouts/, partials/, public/), the fallback rules, and the asset naming convention are almost certainly undocumented in the source's README. That internal spec — not the code — is what survives the fork, and it is what lets a second developer ship a theme without reading the service provider.

Pin and tag. Tag your fork (v1.0.0-internal.1), pin it in composer.json, and put the Laravel-major upgrade cost on the calendar now: roughly one to two engineer-days per major, discovered deliberately rather than mid-upgrade.

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
shitoudev/permissionmanager