Introduction
Where compact() is still useful
Where View Composer is better
Before and after using View Composer
comparision between compact and composer
Practical implemention
Introduction
In Laravel, compact() is very useful when we need to pass page-specific data from a controller to a view. For example, if we are showing one package detail page, one blog detail page, or one user profile page, then compact() is a simple and clean option. But when the same data is needed again and again in many pages, such as navbar menus, footer links, categories, city lists, website settings, or sidebar widgets, using compact() in every controller becomes repetitive and difficult to maintain.
This is where Laravel View Composer becomes useful. View Composer allows us to attach data to a specific view automatically whenever that view is loaded. Instead of passing the same variables from every controller, we can define the shared data once and make it available to layouts, headers, footers, sidebars, dashboards, or common components. This keeps controllers clean and helps developers manage common layout data from one central place.
Where compact() is still useful
compact() is not bad. It is still very useful when data belongs only to one page. For example, if a controller is loading a single blog post, one package detail, one booking invoice, or one user profile, then compact() is simple and readable. The problem starts when developers use compact() for data that belongs to the overall layout instead of the specific page.
Example:
public function show($id)
{
$package = Package::findOrFail($id);
return view('packages.show', compact('package'));
}
In this example, $package is page-specific data. So using compact() is correct.
Where View Composer is better*
View Composer is better when the same data is needed in many views. For example, the website header may appear on every page and may need category data. The footer may appear on every page and may need footer links. The admin sidebar may appear on every admin page and may need role-based menu items. These are not page-specific responsibilities, so they should not be repeated in every controller.
Example:
use Illuminate\Support\Facades\View;
use App\Models\Category;
use App\Models\FooterLink;
use App\Models\Destination;
public function boot()
{
View::composer('layouts.app', function ($view) {
$view->with([
'categories' => Category::active()->get(),
'footerLinks' => FooterLink::active()->get(),
'popularDestinations' => Destination::popular()->limit(10)->get(),
]);
});
}
Now every time layouts.app is loaded, this data will be available automatically. Controllers do not need to pass these variables repeatedly.
Before using View Composer
public function index()
{
$categories = Category::active()->get();
$footerLinks = FooterLink::active()->get();
$popularDestinations = Destination::popular()->limit(10)->get();
return view('home', compact(
'categories',
'footerLinks',
'popularDestinations'
));
}
public function packages()
{
$categories = Category::active()->get();
$footerLinks = FooterLink::active()->get();
$popularDestinations = Destination::popular()->limit(10)->get();
return view('packages.index', compact(
'categories',
'footerLinks',
'popularDestinations'
));
}
Here, the same data is repeated in multiple controller methods.
After using View Composer
public function index()
{
return view('home');
}
public function packages()
{
return view('packages.index');
}
Now the controllers are cleaner because common layout data is handled by View Composer.
comparision between compact and composer
Best practice
Use compact() for data that is unique to a page. Use View Composer for data that belongs to common layout sections. Also, avoid putting heavy database queries directly in View Composer without caching. For common data like categories, footer links, settings, and popular destinations, caching can improve performance.
Example:
View::composer('layouts.footer', function ($view) {
$footerLinks = cache()->remember('footer_links', 3600, function () {
return FooterLink::active()->get();
});
$view->with('footerLinks', $footerLinks);
});
Practical implemention
C:\xampp\htdocs\holidaylandmark\public-site-service\app\Providers\AppServiceProvider.php
View::composer('*', NavbarComposer::class);
*/
public function boot(): void
{
// Custom Auth UserProvider — rehydrates Auth::user() from
// profile-service over HTTP. Registered as driver name
// 'profile_service' (matches config/auth.php providers.users).
Auth::provider('profile_service', function ($app, array $config) {
return new ProfileServiceUserProvider($app->make(ProfileServiceClient::class));
});
// Locale + auth-URL data is needed by the navbar (header partial) AND
// by other partials like home.hero (search-bar country dropdown) and
// home.popular-countries. Registering on '*' makes one Composer the
// single source of truth instead of plumbing data through views.
// Root CLAUDE.md §5.11; the composer pulls from LocaleRepository.
View::composer('*', NavbarComposer::class);
}
C:\xampp\htdocs\holidaylandmark\public-site-service\app\Http\Controllers\HomeController.php
*/
class HomeController extends Controller
{
public function __construct(
private readonly HomePageService $homePageService,
) {}
public function index(Request $request)
{
$page = $this->homePageService->buildPayload($request);
return view('home', compact('page'));
}
}
C:\xampp\htdocs\holidaylandmark\public-site-service\app\View\Composers\NavbarComposer.php
public function compose(View $view): void
{
$view->with([
'active' => $this->locale->resolveActive(
$this->request->query('country'),
$this->request->query('lang'),
$this->request->query('currency'),
),
'countries' => $this->locale->getActiveCountries(),
'languages' => $this->locale->getSupportedLanguages(),
'currencies' => $this->locale->getSupportedCurrencies(),
// Profile-service routes — mounted under holidaylandmark
// via Apache AliasMatch. profile-service decides where to send
// the browser (OIDC redirect, promotion, logout).
'signInUrl' => '/auth/start',
'becomeOrganizerUrl' => '/become-organizer',
'logoutUrl' => '/keycloak/logout',
]);
}
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\views\home.blade.php
@extends('layouts.app')
@section('content')
{{-- $countries / $languages / $currencies / $active / $signInUrl / $becomeOrganizerUrl
are injected globally by App\View\Composers\NavbarComposer — no need
to pass them explicitly to partials that need them. --}}
@include('home.hero', ['hero' => $page['hero']])
@include('home.popular-countries')
@include('home.categories', ['categories' => $page['categories']])
@include('home.discover-trips', ['trips' => $page['trips']])
@include('home.how-it-works', ['steps' => $page['how_it_works'], 'payment_notice' => $page['payment_notice']])
@include('home.trust-features', ['features' => $page['trust_features']])
@include('home.discover-organizers')
@include('home.travel-guides', ['guides' => $page['travel_guides']])
@endsection
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\views\layouts\app.blade.php
<!DOCTYPE html>
<html lang="en" data-theme="holidaylandmark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'HolidayLandmark — See the world through the people who call it home')</title>
<meta name="description" content="HolidayLandmark is a worldwide local tourism marketplace. Discover trips, local guides, homestays, food, transport and authentic experiences offered directly by trusted local organizers.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="min-h-screen bg-cream text-ink font-sans">
@include('partials.header')
@include('partials.profile-completion-banner')
@include('partials.flash-message')
<main>
@yield('content')
</main>
@include('partials.footer')
@livewireScripts
</body>
</html>
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\views\partials\header.blade.php
<a href="{{ $organizerHref }}" class="btn-pill-primary whitespace-nowrap" {{ auth()->guest() ? 'rel=nofollow' : '' }}>Become a Local Organizer</a>
C:\xampp\htdocs\holidaylandmark\profile-service\routes\web.php
Route::get('/become-organizer', function (\Illuminate\Http\Request $request, UserRepository $users) {
$user = $request->user();
if (!$user) {
return redirect()->away(Keycloak::authUrl(null, 'become_organizer'));
}
if (in_array((string) $user->role, ['country_admin', 'admin', 'super_admin'], true)) {
$roleLabel = match ((string) $user->role) {
'super_admin' => 'Super Admin',
'country_admin' => 'Country Admin',
default => 'Admin',
};
return redirect('/')
->with('flash', "You're signed in as {$roleLabel}. Admin accounts can't become organizers — sign out and use a tourist account to register as an organizer.")
->with('flash_type', 'info');
}
$users->promoteToOrganizer($user);
$user->refresh();
if ($user->profile_completed_at) {
return redirect('/');
}
return redirect()->route('profile.complete.show', ['role' => 'organizer']);
})->name('become_organizer');
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\views\partials\flash-message.blade.php
<div
x-data
x-show="$store.flashModal.isOpen"
x-cloak
x-transition.opacity
@keydown.escape.window="$store.flashModal.close()"
class="fixed inset-0 z-[80] bg-ink/50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="flash-modal-title"
@click.self="$store.flashModal.close()"
>
<div
x-show="$store.flashModal.isOpen"
x-transition
class="bg-white rounded-2xl shadow-card-lg w-full max-w-md p-6 sm:p-7 relative"
>
<button
type="button"
@click="$store.flashModal.close()"
class="absolute top-3 right-3 btn btn-ghost btn-sm btn-square"
aria-label="Close"
>
<svg viewBox="0 0 24 24" class="w-5 h-5" fill="none" stroke="currentColor"
stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18"/>
<line x1="6" y1="18" x2="18" y2="6"/>
</svg>
</button>
<div class="flex items-start gap-4">
<span
class="shrink-0 inline-flex items-center justify-center w-11 h-11 rounded-full"
:class="{
'bg-success/10 text-success': $store.flashModal.type === 'success',
'bg-error/10 text-error': $store.flashModal.type === 'error',
'bg-warning/15 text-warning': $store.flashModal.type === 'warning',
'bg-brand-soft text-brand': $store.flashModal.type === 'info'
|| !['success','error','warning'].includes($store.flashModal.type),
}"
>
<svg viewBox="0 0 24 24" class="w-6 h-6"
fill="none" stroke="currentColor" stroke-width="2.2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
</span>
<div class="min-w-0 flex-1 pt-1">
<h2 id="flash-modal-title"
class="font-serif font-semibold text-xl text-ink mb-2 leading-snug"
x-text="$store.flashModal.title"></h2>
<p class="text-sm text-ink-muted leading-relaxed"
x-text="$store.flashModal.message"></p>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-2">
{{-- Dismiss button — always shown --}}
<button type="button"
@click="$store.flashModal.close()"
class="btn-pill-outline"
x-text="$store.flashModal.dismissText"></button>
{{-- Confirm button — only when parent set confirmText --}}
<button type="button"
x-show="$store.flashModal.confirmText"
@click="$store.flashModal.confirm()"
class="btn-pill-primary"
x-text="$store.flashModal.confirmText"></button>
</div>
</div>
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\js\app.js
import './bootstrap';
import registerPaginationStore from './stores/paginationStore';
import registerFlashModalStore from './stores/flashModalStore';
// Alpine is bundled and started by Livewire's @livewireScripts (per TALL-stack
// convention). Do NOT import alpinejs here — Livewire 3 ships its own Alpine
// runtime and running two instances breaks plugins. This file is reserved for
// project-specific Alpine plugins, Livewire customisations, or Vite-bundled
// utilities; we add them here as needed.
// Register reusable Alpine stores before Alpine starts.
document.addEventListener('alpine:init', () => {
registerPaginationStore(window.Alpine);
registerFlashModalStore(window.Alpine);
});
C:\xampp\htdocs\holidaylandmark\public-site-service\resources\js\stores\flashModalStore.js
C:\xampp\htdocs\holidaylandmark\public-site-service\vite.config.js
Top comments (0)