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

Dataloader Php Laravel Package

overblog/dataloader-php

DataLoader is a PHP port of Facebook's DataLoader, providing batching and caching for data fetching. It solves the N+1 query problem by coalescing requests within a single tick, making it ideal for GraphQL resolvers with webonyx/graphql-php and any async or promise-based data layer.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer:

composer require overblog/dataloader-php

Look first at the README, Overblog\DataLoader\DataLoader::create(), DataLoaderInterface, and the CacheMap implementations. The tests are the best source for real batch-function examples. Don’t start by abstracting every model; start with one endpoint that already has a known N+1.

A minimal Eloquent loader:

use App\Models\User;
use Overblog\DataLoader\DataLoader;

$userLoader = DataLoader::create(function (array $ids) {
    $users = User::whereIn('id', $ids)->get()->keyBy('id');

    return array_map(fn ($id) => $users->get($id), $ids);
});

$userLoader->load(1)->then(function (User $user) {
    // use the user
});

Because the loader returns React promises, you must drive the event loop:

use React\EventLoop\Loop;

Loop::run();

First useful case: a PostResource that embeds author for a collection of posts. Instead of one query per post, call loadMany() once, resolve the batch, and attach the users. That is the moment the package earns its keep: N+1 queries collapse to one query per relationship per request.

Implementation Patterns

Bind loaders per request, not globally. Use Laravel’s scoped container binding so the loader cache does not leak between requests:

// AppServiceProvider::register()
$this->app->scoped('dataloader.users', function () {
    return DataLoader::create(function (array $ids) {
        $users = User::whereIn('id', $ids)->get()->keyBy('id');

        return array_map(fn ($id) => $users->get($id), $ids);
    });
});

Honor the batch-function contract. Return an array with one element per input key, in the same order as $ids. Use keyBy() plus array_map() over the original keys so missing rows become explicit null holes. Never return a filtered collection; index alignment is the entire contract.

Load collections with loadMany() plus React\Promise\all():

use function React\Promise\all;

$promises = $posts->map(fn ($post) => $loader->load($post->user_id));

all($promises)->then(function ($users) use ($posts) {
    $posts->each(fn ($post, $i) => $post->setRelation('user', $users[$i]));
});

Loop::run();

For a cleaner synchronous feel, add clue/reactphp-block and use await(all($promises), Loop::get()) instead of sprinkling Loop::run() through controllers.

GraphQL integration. In webonyx/graphql-php, create loaders once per request in the GraphQL context, not inside resolvers, and use the React promise adapter. Sibling field resolvers then share the cache. If you use Overblog’s GraphQLBundle, follow its loader service definitions rather than hand-rolling the wiring.

Treat cache invalidation as explicit. clear($key), clearAll(), and prime($key, $value) are the mutation hooks. After a create/update/delete, clear or prime the relevant loader in the same request. Do not assume the cache expires at the end of a job or worker iteration.

Normalize keys with cacheKeyFn. If IDs arrive as both int and string, set cacheKeyFn to something like (string) $key. Otherwise 1 and '1' become separate cache entries and your batch function sees both.

Use maxBatchSize for large collections. It splits a huge loadMany() into multiple batch calls, which pairs well with array_chunk() in the loader if you hit database placeholder limits.

Test the query count, not just the values. prime() known values to avoid the database entirely, or call the loader, run the loop, and assert one query for ten posts using DB::enableQueryLog() or Telescope. That regression test is what protects the optimization.

Gotchas and Tips

The event loop is the main trap. load() returns a pending React promise; nothing executes until the loop ticks. In a normal FPM request, Loop::run() usually works because DataLoader schedules a one-off future tick. But it blocks until the loop is empty, and it can hang or misbehave if you are already inside a promise callback or another loop. clue/reactphp-block’s await() is generally safer than raw Loop::run().

Octane and Swoole are a real limitation. The React event loop is process-global; running it inside a Swoole worker is not the same as running it in FPM. Never register loaders as singletons, and test under Octane before committing. If you cannot safely drive the loop, this package is the wrong tool; use eager loading (with()/load()) or a synchronous batcher instead.

Batch function order is non-negotiable. Returning User::whereIn(...)->get() directly does not align with $ids. Always map back over the original key list. Missing records must be explicit null values, not omitted entries. A length mismatch will mis-associate results or throw.

Per-key errors need care. A thrown exception from the batch function rejects every key in the batch. To fail one key, return a \Throwable instance in that slot if your version supports it, or resolve the batch with per-key results and handle errors in then/catch. This matters for GraphQL partial responses.

Deduping is intentional. The loader collapses duplicate keys before calling your batch function and caches results. Ten posts by the same author produce one key and one batch entry, not ten. Don’t write the batch function expecting duplicates.

The cache has no TTL by default. It lives as long as the loader instance. After writes, either clear() the changed keys or accept stale data for the rest of the request. For long-running workers, call clearAll() between jobs or provide a custom CacheMapInterface with LRU/TTL behavior via the cacheMap option.

cacheKeyFn must be deterministic and scalar-friendly. Normalize UUIDs, composite keys, and mixed int/string IDs consistently ("user:$id"), not with spl_object_hash or array ordering.

Debug at the batch boundary. Log count($keys) and the resulting SQL inside the batch function. If you see many batches of one key, your load() calls are landing in different ticks, often because they sit inside then callbacks. Move loads earlier, before awaiting, so they collect into one batch.

Extension points are small. The practical ones are cacheMap, cacheKeyFn, maxBatchSize, and wrapping DataLoader::create() in your own factory. There is no Laravel facade or config file; keep the wrapper thin and put domain loaders next to the models or repositories they serve.

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