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():
// 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
// 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
| File | When it runs | Notes |
|---|---|---|
setup/boot.php | Automatically on every front-end request | Required and invoked by FrontServiceProvider::bootTheme() |
setup/seeder.php | Once, 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-loaded | Require 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::creatorto bind view datalisten_blade_insert/add_filterto register hooks and filtersEvent::listen, DB queries, cache reads/writes
What you cannot do:
- Inject into the Panel admin —
bootTheme()lives only inFrontServiceProvider; 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():
| File | Prefix | Notes |
|---|---|---|
routes/root.php | none | No locale prefix, runs in the front middleware group |
routes/front.php | as needed | Mounted under /{locale}/ when multi-language is enabled; route names carry the locale prefix |
<?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 composer | Hook (listen_blade_insert) | |
|---|---|---|
| Essence | Binds data variables to a view | Injects rendered HTML into a slot |
| Rendering | The theme's own template iterates the data | The callback renders its own partial |
| Styling | Reuses the theme's existing design system | The partial must bring its own styles |
| Best for | The block template lives in your theme; it just needs data | Third 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
// 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
// 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
{{-- 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>
@endifCaveats
- Match the exact view name used by the controller.
HomeControllerreturnsinno_view('home', $data), so the composer binds to'home'. A wrong view name makes the composer silently ineffective — always check the controller first. - Keep composers light. A composer runs on every render of that view. Network calls must go through cache (
MarketplaceServicehas it built in) with a sensible TTL. - Always shield exceptions. Return
nullon timeouts or outages and fall back to static content in the template. The homepage must never 500. - Data source prerequisite.
MarketplaceServicerequestsconfig('innoshop.api_url') . '/api/marketplace/*'(theINNOSHOP_API_URLenv), 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, callHttp::get('https://store.example.com/api/marketplace/...')directly in your helper with your own caching. - Boot is not a plugin.
setup/boot.phphas 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:
php artisan view:clearFront-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.
Related Docs
- Theme — theme structure, SCSS architecture, build pipeline
- Hook System — hooks, filters, and available slots
- Plugin — how plugin Boot differs from theme Boot