Install and verify compatibility first. The package's last tagged release is from January 2021, so before anything else check composer.json for its Laravel/PHP constraints and confirm it installs cleanly against your app (Laravel 8/9-era code may need --with-all-dependencies, and Laravel 10/11 support is not guaranteed):
composer require sprocketbox/eloquent-identity
If the package ships a service provider and/or config file, publish it (php artisan vendor:publish) and skim the config before touching models——the defaults are what you'll be living with.
Where to look first. Read the README's trait section, then the package's Concerns/IdentityMap source. There are only two things you actually need to understand: (1) which trait you use on a model to opt it in, and (2) how instances are keyed (almost certainly model class + primary key) and where the map lives (static/global, so it persists for the life of the PHP process).
First use case. Apply the trait to a model you fetch repeatedly in one request:
class Post extends Model
{
use \Sprocketbox\Eloquent\Identity\Concerns\HasIdentity; // confirm exact FQCN in README
}
Then:
$a = Post::find(1);
$b = Post::find(1);
var_dump($a === $b); // true — same object, not two hydrations
That's the whole value proposition: one row, one object, so mutable in-memory state can't diverge between two "copies" of the same record.
Set expectations correctly: this is not a query cache. Every find()/get() still runs SQL; the package only deduplicates the hydrated model. You're buying object identity, not fewer queries.
Request-scoped, not application-scoped. Treat the map as living exactly as long as a single HTTP request. The moment you have a long-lived process (Octane, queue workers, artisan commands that loop), add a flush at the boundary:
// in a middleware terminate(), a job's handle() wrapper, or an Octane listener
app(IdentityMap::class)->flush(); // name per README
Without this, the map grows unboundedly across requests/jobs and you leak memory plus serve stale models between tenants.
Pass models around instead of IDs. The package shines in service/repository code that currently re-fetches to "be safe." With identity mapping you can thread a Post through three collaborators and know that a later Post::find($id) returns the same object any of them mutated. This is where === assertions and spl_object_id() checks become meaningful:
$this->assertSame($post, $service->loadPost($post->id));
Relations and eager loads. When you eager-load $posts->load('author') and separately Author::find($id), identity mapping collapses them into one instance — handy in view composers and API resources that touch the same parent from multiple places.
Testing. This is arguably the best day-to-day use: deterministic object identity in feature tests, so assertSame works and mutation assertions don't depend on which query loaded the model first. Add a flush() in setUp() (or a base TestCase) so tests stay isolated.
Custom keys. If you use UUIDs or composite keys, check whether key resolution is overridable before you commit — a package this small likely assumes a single auto-increment getKey(), and you don't want to discover that after wiring it into ten models.
Post::select('id','name')->find(1) may register a model missing columns; a later Post::find(1) returns that same incomplete instance instead of a full row. Avoid column-limited selects on identity-mapped models, or flush around them.DB::table('posts')->update(...), a second connection, a queued job in another worker, or a cron job will all leave you reading stale attributes. For those paths, refresh()/fresh() behaviour is critical — verify empirically whether fresh() returns the cached instance or bypasses the map. That's the single most important thing to test before relying on this.refresh() vs. the map. Confirm that refresh() actually reloads from the DB and updates the cached instance, rather than returning the stale cached object. Write one test for this; it will save you a production incident.withTrashed(), onlyTrashed(), and withoutGlobalScopes() interplay is a classic identity-map edge case — deleting a model should remove it from the map, otherwise a trashed row keeps appearing.chunk()/cursor() memory wins are lost. The map retains every hydrated model by design, so streaming large tables becomes more memory-hungry, not less. Flush between chunks for bulk jobs.count($map) and spl_object_id() before/after a fetch; confirm you're actually hitting the cache and not a second instance. If === is still false, the trait isn't on the model or the map was flushed between calls.composer.json, and expect to own the source if Laravel changes newFromBuilder()/Builder internals. Budget for a fork-and-patch path, and check the repo's issue tracker for the Laravel version matrix before you build a design around it.How can I help you explore Laravel packages today?