Two ways to get data into a view without threading it through every controller that renders one. A composer runs when a specific view is rendered; shared data is available to every view in the application, always.
public function hasViewComposer(string|array $views, string|Closure $composer): staticpublic function hasSharedDataForAllViews(array $viewSharedData): static
View composers
use Acme\Blog\View\Composers\SidebarComposer; $packager ->name('Blog') ->hasViews() ->hasViewComposer('blog::sidebar', SidebarComposer::class);
namespace Acme\Blog\View\Composers; use Acme\Blog\Repositories\PostRepository;use Acme\Blog\Models\Category;use Illuminate\View\View; class SidebarComposer{ public function __construct(private PostRepository $posts) {} public function compose(View $view): void { $view->with('categories', Category::withCount('posts')->get()); $view->with('recent', $this->posts->latest(limit: 5)); }}
<aside> @foreach ($categories as $category) <a href="{{ route('blog.category', $category) }}"> {{ $category->name }} ({{ $category->posts_count }}) </a> @endforeach</aside>
The composer class is resolved from the container, so constructor injection works.
Closures
For something small, skip the class:
$packager->hasViewComposer('blog::sidebar', function (View $view) { $view->with('categories', Category::all());});
Closures and config:cache
A closure composer is registered at boot and lives in memory, so it is unaffected by config caching.
But a closure captured in a property of a serialisable object is not — keep composer closures inside
configure() and they are fine.
Several views at once
$packager->hasViewComposer( ['blog::sidebar', 'blog::footer', 'blog::partials.nav'], SidebarComposer::class,);
Wildcards
Laravel's own wildcard syntax works, since the value is passed straight to View::composer():
$packager->hasViewComposer('blog::*', BlogComposer::class); // every package view$packager->hasViewComposer('blog::admin.*', AdminComposer::class); // one subdirectory$packager->hasViewComposer('*', GlobalComposer::class); // every view — see below
'*' runs for every view in the application
Including views that have nothing to do with your package. A package that registers a global composer imposes its cost on every render in the host application. Scope it to your own namespace unless you have a specific reason not to.
Registering more than one
Composers accumulate per view, keyed by the view name — so registering two composers for the same view keeps only the last:
$packager ->hasViewComposer('blog::sidebar', CategoryComposer::class) ->hasViewComposer('blog::sidebar', RecentPostsComposer::class); // Only RecentPostsComposer is registered for blog::sidebar.
Different views accumulate normally:
$packager ->hasViewComposer('blog::sidebar', SidebarComposer::class) // ✓ ->hasViewComposer('blog::footer', FooterComposer::class); // ✓
If one view genuinely needs two composers, combine them into one class, or call
View::composer() directly from a lifecycle hook.
Shared data
$packager ->name('Blog') ->hasSharedDataForAllViews([ 'blogName' => 'Acme Blog', 'blogVersion' => '2.4.0', 'blogSettings' => ['theme' => 'default', 'rtl' => false], ]);
{{-- available in every view in the application --}}<footer>{{ $blogName }} v{{ $blogVersion }}</footer>
Allowed values
Keys must be strings. Values must be scalar, array, null, or an instance of
Illuminate\Contracts\Support\Arrayable. Anything else throws InvalidArgumentException during
registration:
$packager->hasSharedDataForAllViews([ 'blogName' => 'Acme Blog', // ✓ string 'blogPerPage' => 15, // ✓ int 'blogFlags' => ['beta' => true], // ✓ array 'blogTheme' => null, // ✓ null 'blogCategories' => Category::all(), // ✓ Arrayable 'blogFormatter' => fn () => …, // Closure 'blogRepository' => new PostRepository(), // object ]);
The restriction exists to stop packages sharing service objects into the view layer — a habit that turns templates into an untestable second application.
Eloquent queries run at boot
Category::all() in the array above executes during boot(), on every request, whether or not
any view uses it. That is a database query added to your artisan commands, your queue workers and
your health check. Use a view composer instead — it runs only when the view that
needs the data is actually rendered.
Merging
Calls accumulate, and a repeated key wins on the later call:
$packager ->hasSharedDataForAllViews(['blogName' => 'Blog']) ->hasSharedDataForAllViews(['blogVersion' => '2.4.0', 'blogName' => 'Acme Blog']); // ['blogName' => 'Acme Blog', 'blogVersion' => '2.4.0']
Prefix your keys
Shared data is a flat global namespace shared with the application and every other package. A
package that shares $settings will one day silently overwrite something.
'settings' => … // one day, somebody else's 'blogSettings' => …
Choosing between them
| Composer | Shared data | |
|---|---|---|
| Runs | when a matching view renders | at boot, always |
| Scope | the views you name | every view |
| Suitable for | queries, request-dependent values | constants, static config |
| Cost when unused | none | paid on every request |
The rule of thumb: if computing the value costs anything at all, it belongs in a composer.
Introspection
$packager->isViewComposable(); // bool$packager->viewComposers(); // ['view name' => composer]$packager->isSharedWithViews(); // bool$packager->viewSharedData(); // ['key' => value]
Testing
test('the composer binds categories to the sidebar', function () { Category::factory()->count(3)->create(); expect(view('blog::sidebar')->render())->toContain('Announcements');}); test('shared data reaches an application view', function () { expect(Blade::render('{{ $blogName }}'))->toBe('Acme Blog');});