Skip to content

Theme Boot & Dynamic Data Injection

Overview

A theme can do more than override templates and styles. Via setup/boot.php it can register runtime logic during application boot: bind data to views, listen to hooks, subscribe to events, and more. This guide covers the theme boot mechanism, theme-bundled routes, and a real-world case — pulling recommended plugins/themes from the official marketplace into the homepage — including the View composer vs Hook decision.

Typical use cases:

  • Show recommended plugins and latest themes from the official marketplace on the homepage (no plugin required)
  • Fetch dynamic data (exchange rates, news feeds) from external APIs and inject it into theme templates
  • Give the theme its own front-end routes (landing pages, custom pages)

How Boot Works

themes/{theme}/setup/boot.php is loaded automatically on every request by FrontServiceProvider::bootTheme():

php
// innopacks/front/src/FrontServiceProvider.php
protected function bootTheme(): void
{
    $currentTheme = system_setting('theme');
    if (! $currentTheme) {
        return;
    }

    $bootFile = base_path("themes/{$currentTheme}/setup/boot.php");
    if (! is_file($bootFile)) {
        return;
    }

    $boot = require $bootFile;
    if (is_callable($boot)) {
        $boot();
    }
}

The convention: the file returns a closure which is invoked immediately. A missing file is silently skipped.

php
<?php
// themes/my-theme/setup/boot.php

use Illuminate\Support\Facades\View;

return function () {
    // Register view composers, hook listeners, event listeners here
};

How the setup/ directory is loaded

FileWhen it runsNotes
setup/boot.phpAutomatically on every front-end requestRequired and invoked by FrontServiceProvider::bootTheme()
setup/seeder.phpOnce, when the admin runs "Import theme demo data"Triggered by the Panel-side ThemeDemoService (innopacks/panel/src/Services/ThemeDemoService.php), not by boot
Anything else (e.g. helpers.php)Not auto-loadedRequire it yourself from boot.php

Boot Capabilities and Limits

What you can do (inside the boot closure):

  • Register routes (or use the routes/ directory)
  • View::composer / View::creator to bind view data
  • listen_blade_insert / add_filter to register hooks and filters
  • Event::listen, DB queries, cache reads/writes

What you cannot do:

  • Inject into the Panel adminbootTheme() lives only in FrontServiceProvider; admin requests never execute the theme boot, so a theme cannot inject into the admin UI
  • Register middleware — themes have no middleware mechanism (plugins do); if you need to intercept the request flow, build a plugin
  • Lifecycle management — no install/uninstall/upgrade hooks, no admin settings page (see "Boot is not a plugin" below)
  • Exception isolation — an exception thrown inside the boot closure turns into a front-end 500; wrap unreliable calls in try/catch yourself

Timing and theme switching: boot runs in the service provider boot phase and resolves the current theme from system_setting('theme') on every request. After switching themes in the admin, the new boot takes effect on the next request; the old theme's boot/composers/hooks simply stop running — no cleanup needed.

Theme-Bundled Routes

A theme may ship two route files, registered automatically by FrontServiceProvider::loadThemeRoutes():

FilePrefixNotes
routes/root.phpnoneNo locale prefix, runs in the front middleware group
routes/front.phpas neededMounted under /{locale}/ when multi-language is enabled; route names carry the locale prefix
php
<?php
// themes/my-theme/routes/front.php

use Illuminate\Support\Facades\Route;

Route::get('/campaign', fn () => inno_view('pages.campaign'))->name('campaign');

Choosing the Injection Method: View composer vs Hook

The two most common injection techniques in a theme boot solve different problems:

View composerHook (listen_blade_insert)
EssenceBinds data variables to a viewInjects rendered HTML into a slot
RenderingThe theme's own template iterates the dataThe callback renders its own partial
StylingReuses the theme's existing design systemThe partial must bring its own styles
Best forThe block template lives in your theme; it just needs dataThird parties injecting into someone else's template

Decision rule:

  • The block template already lives in your theme (e.g. home/plugins.blade.php) → use a View composer to fetch data and let the template render it. Shortest path, no style fragmentation.
  • You want other plugins to be able to inject content into your theme pages (an extensible slot) → leave a @hookinsert('home.xxx.extra') slot in the template and let plugins provide callbacks.
  • Both can coexist: the template iterates real data, with a trailing hook slot for the ecosystem.

Real World: Marketplace Recommendations on the Homepage

Take the innointl theme: home/plugins.blade.php and home/themes.blade.php showcase the latest recommended plugins and themes from the official marketplace — without depending on any plugin, because the data client MarketplaceService ships with the core package innopacks/plugin.

Step 1: Register a composer in boot.php

php
<?php
// themes/innointl/setup/boot.php

use Illuminate\Support\Facades\View;

require __DIR__.'/helpers.php';

return function () {
    // Match the exact view name the controller returns: HomeController returns inno_view('home')
    // For multiple pages sharing the same data, pass an array: View::composer(['home', 'plugins.index'], ...)
    View::composer('home', function ($view) {
        $view->with('marketPlugins', innointl_market_products('plugins', 6));
        $view->with('marketThemes', innointl_market_products('themes', 6));
    });
};

Step 2: Wrap data fetching (with cache and fallback)

MarketplaceService::getMarketProductsWithParams() has built-in caching (cache key hashed from query params, configurable TTL and store). Wrap it with exception shielding so the homepage never breaks when the marketplace is down.

Note: functions defined in setup/ live in the global namespace. Prefix them with the theme code and guard with function_exists, otherwise coexisting or switched themes will hit a fatal Cannot redeclare error:

php
<?php
// themes/innointl/setup/helpers.php (required from boot.php; NOT auto-loaded)

use InnoShop\Plugin\Services\MarketplaceService;

if (! function_exists('innointl_market_products')) {
    function innointl_market_products(string $type, int $limit): ?array
    {
        try {
            $result = MarketplaceService::getInstance()
                ->setPerPage($limit)
                ->getMarketProductsWithParams(['parent_slug' => $type]);

            return $result['data'] ?? null;
        } catch (\Throwable $e) {
            return null; // fall back when the marketplace is unreachable
        }
    }
}

Step 3: Template iteration with fallback

blade
{{-- themes/innointl/views/home/plugins.blade.php --}}

@if($marketPlugins)
  <div class="marketplace-grid">
    @foreach($marketPlugins as $item)
      <a href="{{ front_route('plugins.index') }}" class="marketplace-card">
        <div class="card-cover">
          <img src="{{ $item['image'] }}" alt="{{ $item['name'] }}">
        </div>
        <div class="card-body">
          <div class="card-name">{{ $item['name'] }}</div>
          <p class="card-excerpt">{{ $item['summary'] }}</p>
        </div>
      </a>
    @endforeach
  </div>
@else
  {{-- Static fallback when marketplace data is unavailable --}}
  <div class="marketplace-grid">
    {{-- original static cards ... --}}
  </div>
@endif

Caveats

  1. Match the exact view name used by the controller. HomeController returns inno_view('home', $data), so the composer binds to 'home'. A wrong view name makes the composer silently ineffective — always check the controller first.
  2. Keep composers light. A composer runs on every render of that view. Network calls must go through cache (MarketplaceService has it built in) with a sensible TTL.
  3. Always shield exceptions. Return null on timeouts or outages and fall back to static content in the template. The homepage must never 500.
  4. Data source prerequisite. MarketplaceService requests config('innoshop.api_url') . '/api/marketplace/*' (the INNOSHOP_API_URL env), which must point to a deployed official marketplace (a site with the InnoSite / InnoOfficial plugins installed). To pin the data source regardless of the site-wide config, call Http::get('https://store.example.com/api/marketplace/...') directly in your helper with your own caching.
  5. Boot is not a plugin. setup/boot.php has no lifecycle management (no install/uninstall/upgrade hooks) and no admin settings page. If you need configuration UI, database migrations, or scheduled tasks, build a plugin instead.

Troubleshooting

Composer registered but never fires Nine times out of ten the view name is wrong. Check what the controller actually returns (inno_view('xxx') in the source). A composer bound to the wrong name fails silently — no error at all.

Edited a theme Blade file but the page didn't change Blade decides whether to recompile based on file mtime. Syncing theme files with rsync -a / cp -p preserves timestamps, so the stale compiled cache keeps being served. Run:

bash
php artisan view:clear

Front-end 500 right after enabling the theme Exceptions inside the boot closure have no isolation layer and bubble up as a site-wide front-end 500. Check storage/logs/laravel.log and wrap unreliable operations (HTTP calls, file I/O) in try/catch.

Fatal: Cannot redeclare helper function PHP files under setup/ define global functions. If two themes both define theme_market_products(), the one loaded later fatals. Use theme-prefixed names with a function_exists guard (see the example above).

No boot effect in the admin panelbootTheme() runs only in FrontServiceProvider (front end). Panel admin requests never load the theme boot. This is by design, not a bug.

  • Theme — theme structure, SCSS architecture, build pipeline
  • Hook System — hooks, filters, and available slots
  • Plugin — how plugin Boot differs from theme Boot