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

Laravel Inertia Shared Data Provider Laravel Package

ebess/laravel-inertia-shared-data-provider

A Laravel package for defining shared data providers for Inertia.js, letting you resolve common props once and pass them to every page without repeating code across controllers. Keeps Inertia responses consistent and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a single 1.0.0 release from November 2021 with a thin README, so budget the first hour for reading the source rather than the docs. It is a small, focused package — that hour is usually enough to have the whole surface in your head.

Minimal steps

  1. Install and pin the minor line: composer require ebess/laravel-inertia-shared-data-provider:^1.0. Because there is exactly one tag, pinning to 1.0.0 for the first deploy is defensible; loosen to ^1.0 once you have a green production run.
  2. Read composer.json in the package before anything else. You need to know its illuminate/* floor, its PHP floor, and — critically — whether it constrains inertiajs/inertia-laravel. A 2021 package was written against Inertia Laravel 0.4–0.6; confirm your installed version is compatible before wiring it into HandleInertiaRequests.
  3. Read src/ in this order: the contract(s) (src/Contracts), the base provider if one ships, the service provider (register() for bindings, boot() for anything it publishes or registers), and any config file. The whole package is small enough to read end to end; do it.
  4. Confirm discovery: composer dump-autoload, then check bootstrap/cache/packages.php (Laravel 11: php artisan about) for the provider. Publish config if the package ships one: php artisan vendor:publish --provider="Ebess\\...\\ServiceProvider" — use the exact class name from composer.json's extra.laravel.providers.
  5. Build one provider and wire it into HandleInertiaRequests::share() as the only change in that PR.

Where to look first

  • The contract: what a provider must implement (an identifier/key plus a resolver is the usual shape).
  • The service provider: how providers are collected — config array, container tag, or explicit register() calls. This determines your whole integration style.
  • The resolution point: whether the package returns an array you merge into share(), or whether it calls Inertia::share() itself. That changes how ordering and collisions behave.

First use case

Move auth.user out of the inline closure in app/Http/Middleware/HandleInertiaRequests.php into a dedicated UserDataProvider. Nothing else. You get a before/after you can diff, one place to assert on, and a working test proving the prop still reaches the frontend. Once that is green, migrate flash and your nav counters the same way, one provider per PR.

The class/interface names below are indicative — confirm the exact namespace and method names in src/ before copying. The README does not spell them out.

// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        // was: 'user' => fn () => $request->user()?->only('id', 'name'),
        ...$this->sharedData->resolve($request),
    ]);
}

Implementation Patterns

One provider per concern, not per page

The whole value of the package is that a provider is a unit you own and can test. Split along data ownership boundaries: UserDataProvider, FlashDataProvider, NotificationsDataProvider, FeatureFlagDataProvider, TenantDataProvider. If a provider needs the word "and" to describe it, it is two providers.

Resolve inside the closure, not at construction

The classic bug in a provider class is capturing state in the constructor that is not valid at response time:

final class UserDataProvider implements SharedDataProvider
{
    // BAD: resolved when the container builds the provider
    public function __construct(private ?User $user) {}

    // GOOD: resolve lazily, per request
    public function data(Request $request): mixed
    {
        return fn () => $request->user()?->only('id', 'name', 'email');
    }
}

Providers are typically resolved and called during the Inertia response, so a constructor-injected Request may work — but a constructor-injected Auth::user() snapshot will bite you in queue workers, Octane, and tests. Always resolve from the request inside the resolver.

Register the cheap ones eagerly and the expensive ones as closures

Shared data is evaluated on every Inertia response — that is the defining cost of this package. Keep the unconditionally-shared props small (user identity, flash, a couple of flags) and push anything that touches an aggregate query or an external service behind a closure, or out of shared data entirely and into per-page props.

final class NavDataProvider implements SharedDataProvider
{
    public function key(): string { return 'nav'; }

    public function data(Request $request): mixed
    {
        return fn () => [
            'unreadCount' => Cache::remember(
                "nav.unread.{$request->user()->id}",
                now()->addMinute(),
                fn () => $request->user()->notifications()->unread()->count(),
            ),
        ];
    }
}

Compose at the middleware, keep ordering explicit

Whichever registration mechanism the package offers (config array is the common one), treat the resolved array as an ordered merge and know who wins on duplicate keys. Do not rely on alphabetical or directory order — write the list explicitly so a new provider cannot silently shadow user.

// config/inertia-shared-data.php (shape — confirm key names against the source)
'providers' => [
    \App\Inertia\Shared\UserDataProvider::class,
    \App\Inertia\Shared\FlashDataProvider::class,
    \App\Inertia\Shared\FeatureFlagDataProvider::class,
],

Gate per-provider logic on the request, not on the environment

A provider that needs "only when authenticated", "only for tenant X", or "only when the feature flag is on" should decide that inside data() and return an empty array or null, so the decision is unit-testable. Registering/unregistering providers per environment is harder to reason about and will surprise you in the test suite.

public function data(Request $request): mixed
{
    if (! $request->user()?->isStaff()) {
        return [];
    }
    return fn () => ['staffTools' => true];
}

Test the provider in isolation, then assert once end-to-end

This is the second real payoff. The provider needs no HTTP request to be tested if you feed it a request you construct, and the middleware needs only one assertion to prove wiring.

it('exposes a minimal user payload', function () {
    $user = User::factory()->create(['name' => 'Ada']);
    $request = Request::create('/')->setUserResolver(fn () => $user);

    $data = (new UserDataProvider())->data($request);
    expect($data())->toBe(['id' => $user->id, 'name' => 'Ada', 'email' => $user->email]);
});

it('wires shared data into every page', function () {
    $this->actingAs(User::factory()->create())
        ->get('/dashboard')
        ->assertInertia(fn (Assert $page) => $page->where('user.name', 'Ada'));
});

Wrap the package behind one app-owned class

Even a well-scoped dependency earns an adapter if the exit cost matters to you. One App\Inertia\SharedData class that owns the provider list and returns the merged array means swapping, forking, or reimplementing is a one-file change. With a single-maintainer 2021 package, that insurance is cheap.

final class SharedData
{
    public function __construct(private SharedDataFactory $factory) {}

    public function resolve(Request $request): array
    {
        return collect(config('inertia-shared-data.providers'))
            ->map(fn (string $class) => app($class))
            ->reduce(fn (array $props, $provider) => $props + [$provider->key() => $provider->data($request)], []);
    }
}

Deploy it behind a flag the first time

if (! config('features.shared_data_providers')) { return parent::share($request); } in the middleware. New shared-data code fails in production for boring reasons: a provider throws on the 404 page, a config cache is stale, a prop leaks a field you forgot was not $hidden. A flag turns a rollback into a config flip instead of a revert-and-deploy.

Gotchas and Tips

A 2021 package means 2021 assumptions

1.0.0 predates Inertia's deferred props, Laravel 10/11, and PHP 8.2. Nothing in the design is wrong for today, but nothing has been tested against today either. Before committing, run the package's own suite (if it ships one) against your Laravel/PHP matrix, and confirm the illuminate/support constraint actually allows your framework version — Composer will let you in via a loose constraint and you will find out at runtime.

Shared data runs on every Inertia response — including the ones you forgot about

Login, register, password reset, 404, 419, and tenant-not-found pages all resolve shared props. A provider that does $request->user()->id unguarded will 500 the login page. Guard every auth assumption with ?-> and return [] early.

Do not put closures in a config file

If registration is config-driven, register class-strings, never closures. php artisan config:cache serializes the config array and will fail (or silently drop) closures. Symptom: works in local, broken in production where the config was cached. Same rule for env() — the package's config should call env(), your code should call config().

Merge order and key collisions are silent

Two providers returning user, or a package-provided user colliding with one still inline in share(), resolves to "whoever merged last" with no warning. If you are migrating incrementally, delete the inline entry in the same commit you add the provider, and add one assertInertia test per key so a collision shows up as a failing assertion rather than a mystery in the frontend.

Serialization leaks

Returning an Eloquent model works, serializes everything not in $hidden, and will eventually ship an email_verified_at or an internal_notes column to the browser. Return ->only([...]) or an API Resource from the provider. If you use Model::preventLazyLoading(), also check that a provider is not the thing triggering a lazy load on a prop you never intended to send.

N+1 across providers

Three providers each querying $request->user() relations is three queries per page load. Cache the derived values (per-user key, short TTL) or consolidate them into one provider that does a single withCount. Measure before you assume the package's "clean separation" is free.

Partial reloads and lazy props behave differently across Inertia versions

Whether shared props are excluded by only/except partial reload headers, and when closures get evaluated, has changed between inertia-laravel releases. Do not reason about it from memory. Test one page with router.reload({ only: ['somethingElse'] }) and look at the JSON.

Debugging checklist

  • php artisan tinker → app(YourProvider::class)->data(request()) to see exactly what the provider returns outside the middleware.
  • Dump the raw payload rather than relying on the browser: curl -s -H 'X-Inertia: true' -H 'X-Requested-With: XMLHttpRequest' <url> returns the Inertia JSON with the merged shared props.
  • A prop that is null in production and correct locally is almost always config:cache — check for closures/direct env().
  • If a provider is not being called at all: bootstrap/cache/packages.php, dont-discover in your root composer.json, and whether you ran dump-autoload after install.
  • If the middleware throws only on error pages, your provider is unguarded for guests — the stack trace will point at the provider, not the middleware.

Extension points

The contract itself is the extension point: implement it, decorate it (a CachedProvider wrapping another provider is a clean way to add memoization without touching providers), or bind a NullProvider/fake in tests. There is no driver Manager, no event surface, and no config-merge magic beyond what you saw in src/ — so your own adapter class is the real seam. Write it on day one, and migrating off this package later stays a one-file diff.

Exit cost is low — keep it that way

The feature is small enough to reimplement in a middleware in well under a day (collect($providers)->map->data()). That is the strongest argument for adopting it and the reason you should not let provider classes leak package types into controllers, jobs, or resources. Keep the dependency at exactly one edge: HandleInertiaRequests.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource