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 Jit Loader Laravel Package

liam-wiltshire/laravel-jit-loader

Laravel JIT Loader adds just-in-time relationship loading to Eloquent, helping eliminate N+1 queries without manually eager-loading every relation with with(). It loads relationships only when needed, keeping model access simple and query-efficient.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require liam-wiltshire/laravel-jit-loader

There is nothing to publish and nothing to register if you're on Laravel 5.5+ package discovery. The whole package is a single trait.

Wire up your first model

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use LiamWiltshire\LaravelJitLoader\JitLoader;

class Post extends Model
{
    use JitLoader;

    protected $jit = ['author', 'comments'];
}

That's the entire setup. Any query that hydrates Post models — Post::all(), Post::where(...)->get(), Post::paginate(), route model binding — will now come back with author and comments already attached.

Where to look first

Open vendor/liam-wiltshire/laravel-jit-loader/src/JitLoader.php. It's a few dozen lines and knowing the exact hook point is what separates "it works" from "why did my query count triple". Confirm:

  • Which model lifecycle method/event it intercepts (this determines when relations are resolved).
  • Whether it guards with relationLoaded() before eager loading (matters when you also call with() explicitly).
  • Whether there is any opt-out flag or static toggle you can use for edge cases.

First real use case

The classic admin index page:

// Controller — no with() anywhere
$posts = Post::latest()->paginate(25);

// resources/views/posts/index.blade.php
@foreach ($posts as $post)
    {{ $post->author->name }}   {{-- 1 query, not 25 --}}
@endforeach

Before: 26 queries. After: 3. Verify with DB::enableQueryLog() or Telescope.

Don't start everywhere. Pick two or three models that appear in list views and are read far more often than written. Measure before and after.


Implementation Patterns

Baseline the "read model" of your app

Treat $jit as documentation of a model's default read shape. If a relation is rendered on 90% of the screens that touch the model, it belongs in $jit. If it's shown on a detail page only, it does not.

class Product extends Model
{
    use JitLoader;

    // shown on every card, everywhere
    protected $jit = ['brand', 'primaryImage'];
}

Detail pages that need reviews, variants, supplier should keep using explicit with():

$product = Product::with('reviews.user', 'variants')->findOrFail($id);

The two mechanisms compose: $jit covers the baseline, with() covers the specifics of one screen.

Centralize instead of scattering

The value proposition is that relationship decisions move out of controllers and repositories and into the model. Push in that direction consistently — don't add $jit and keep the same with() calls in every controller, or you'll double up.

Repository pattern still applies cleanly:

class PostRepository
{
    public function published(): LengthAwarePaginator
    {
        // no with() needed; $jit covers it
        return Post::where('published', true)->latest()->paginate();
    }

    public function withComments(): Builder
    {
        // override/extend when a screen genuinely needs more
        return Post::with('comments.replies');
    }
}

Nested models and cascading

$jit is per-model, so a related model can declare its own:

class Comment extends Model
{
    use JitLoader;
    protected $jit = ['author'];
}

class Post extends Model
{
    use JitLoader;
    protected $jit = ['comments'];
}

Be deliberate about this — a Post list now loads comments and each comment's author. That can be the right answer (comment list UI) or a silent performance regression (post card only needs comments_count). Watch the query count when you add a nested $jit; it applies everywhere the inner model is hydrated, including from inside eager loads.

Pair with lazy-loading prevention

This package and Model::preventLazyLoading() are complementary:

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

With $jit doing its job, common relations never trip the guard. Relations you forgot to declare still throw, which is exactly the signal you want. Use this combination in local dev and CI — it surfaces gaps in your $jit declarations immediately.

Test the query count, not just the output

Because the package works invisibly, a broken $jit (typo, removed relation) still returns correct data — just with N+1s. Assert on queries:

public function test_post_index_has_no_n_plus_one(): void
{
    Post::factory()->count(10)->hasComments(2)->create();

    DB::enableQueryLog();
    $posts = Post::latest()->take(10)->get();
    $posts->each(fn ($p) => $p->author->name);
    $posts->each(fn ($p) => $p->comments->count());

    $this->assertLessThanOrEqual(4, count(DB::getQueryLog()));
}

Run this whenever someone edits $jit.

Migration path

Incremental rollout works well:

  1. Add the trait to one hot model.
  2. Run the test suite with query-count assertions and preventLazyLoading on.
  3. Compare query logs on a staging dataset (not a 3-row seeder — the win only shows at scale).
  4. Repeat.

If something goes wrong on a single model, remove the trait from that one class; nothing else depends on it.


Gotchas and Tips

Over-fetching is the default failure mode

$jit fires on every hydration, including:

  • Post::count()-style aggregates — usually harmless, but check whether the package skips them.
  • Post::find($id) in a background job that only needs id and status.
  • Models hydrated inside each() loops on huge collections.

If you can't find a built-in opt-out in the trait source, the pragmatic escape hatches are: a subclass with an empty $jit, a query-builder macro, or dropping the select() down to what the JIT loader needs.

select() with a narrow column list breaks

This is the sharpest edge:

Post::select('id', 'title')->get(); // $jit wants author_id, user_id, etc.

Eager loading a belongsTo requires the foreign key on the parent; a hasMany requires the parent's primary key. Restricting columns silently produces null relations or SQLSTATE[42S22]: Column not found errors, and the stack trace points at the eager-load query, not at your select(). Always include the keys the JIT loader needs, or don't narrow the select on that query.

Infinite recursion between mutually-declaring models

If Post::$jit = ['author'] and User::$jit = ['posts'], and the package does not guard against already-loaded relations, you can get runaway eager loading. Whether this actually recurses depends on the hook implementation (one level of eager loading is usually safe), but it is the first thing to test when you add $jit to both sides of a relationship. Check the trait source for a relationLoaded() / depth guard before shipping mutual declarations.

Typos fail at runtime, far from the declaration

protected $jit = ['auther']; throws RelationNotFoundException on the first query that hydrates the model — often in an unrelated test or a queued job. Mitigations:

  • Add a cheap test that instantiates each model and calls $model->{ $relation }() for each entry in $jit to assert the method exists.
  • Or a single Pest/PHPUnit test iterating model classes with the trait and reflecting $jit.

$jit is inherited

Subclasses inherit the parent's $jit. If you have STI-style hierarchies or User/AdminUser extends User, either override $jit explicitly in the child or accept the parent's list.

Serialization changes silently

Adding $jit changes toArray() / toJson() / API resource output — relations that were previously absent now appear. That's a breaking change for API consumers. Audit JsonResource classes and any place that returns a model directly from a controller.

Package age and framework compatibility

Last release is November 2020, so it targets the Laravel 6–8 era. Before adopting:

  • Check the composer.json constraints and run the suite on your actual Laravel version.
  • Test on your PHP version (8.1+ deprecations around dynamic properties are unlikely here since $jit is declared, but verify).
  • Confirm it still hooks correctly if your app uses Illuminate\Database\Eloquent\Builder::lazy(), chunkById(), or cursor(). Streaming APIs that hydrate one model at a time are the most likely to behave differently — measure query counts on a lazy()/cursor() path specifically.

Debugging the invisible

  • ->toSql() shows only the base query; eager loads are separate queries.
  • DB::listen() or DB::enableQueryLog() is the reliable way to see what JIT added.
  • Telescope's query tab groups the eager-load queries after the parent query — that's the fingerprint.
  • If a page got slower, check whether $jit is loading a heavy relation (large hasMany, or a relation with its own with() chain) on a screen that never displays it.

Extension points

  • Override $jit in a child class for context-specific behavior.
  • Combine with with() per query — explicit with() should win or merge; verify no duplicate eager loads by watching the query log.
  • Global scopes and soft deletes on $jit relations apply as normal, so a soft-deleted author comes back as null, not an exception. Handle with ?-> in views.
  • If you need conditional JIT behavior, the cleanest long-term move is often to replace the trait on that one model with an explicit with() in the repository — the package is a convenience, not a framework.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel