jenssegers/mongodb
This package brings MongoDB support to Laravel's Eloquent model and query builder, using the original Laravel API and methods so you can keep the same fluent syntax. Formerly jenssegers/mongodb, it is now mongodb/laravel-mongodb and supports Laravel 10.x as a drop-in replacement.
1. Install the driver + package. MongoDB needs the PHP extension before Composer can do anything useful:
pecl install mongodb # or your distro package / docker-php-ext-install
composer require mongodb/laravel-mongodb
Note the name: jenssegers/mongodb is the legacy handle. Since MongoDB, Inc. took ownership the installable package is mongodb/laravel-mongodb and the namespaces are MongoDB\Laravel\.... Check the compatibility table before pinning — 5.9 dropped Laravel 10, 11 and PHP 8.1, so a "latest" install can silently force a framework upgrade.
2. Configure the connection. Add a connection to config/database.php (not a new config file) and point DB_CONNECTION at it:
'mongodb' => [
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://127.0.0.1:27017'),
'database' => env('MONGODB_DATABASE', 'app'),
'options' => [
// 'replicaSet' => 'rs0', 'readPreference' => 'secondaryPreferred',
],
],
The service provider is auto-discovered and registers the mongodb driver plus the cache, session, queue, batch and filesystem extensions.
3. First model. Extend the package's model, not Illuminate\Database\Eloquent\Model:
use MongoDB\Laravel\Eloquent\Model;
class Article extends Model
{
protected $connection = 'mongodb';
protected $collection = 'articles'; // optional; defaults to pluralized table name
protected $fillable = ['title', 'tags', 'author'];
}
Then php artisan tinker and run Article::create([...]), Article::first(), Article::where('tags', 'php')->get(). If that works, the rest of Eloquent works.
Where to look first: the official docs at mongodb.com/docs/drivers/php/laravel-mongodb/ (the README is thin), your new config/database.php entry, and — if you use Laravel Boost or coding agents — the resources/boost/skills/laravel-mongodb/SKILL.md skill shipped in the repo, which is written specifically for consumers of the package rather than contributors.
Best first use case: a read-heavy, schema-flexible slice of the app — event/log storage, product catalog with varying attributes, CMS content, or a job queue — rather than migrating your whole relational core on day one.
Query with Mongo semantics deliberately. The builder accepts dot notation and Mongo operators, so lean into it instead of transliterating SQL:
Article::where('author.id', $id)
->where('tags', 'all', ['php', 'laravel'])
->where('published_at', '>=', now()->subWeek())
->project(['title' => 1, 'tags' => 1])
->orderBy('published_at', SortDirection::Descending) // enum supported since 5.8
->limit(20)
->get();
Article::whereRaw(['$expr' => ['$gt' => ['$likes', '$shares']]])->get();
Use ->push(), ->pull() and ->unset() on the builder for atomic array/field mutations instead of read-modify-write.
Aggregation via the raw collection. For $lookup, $facet, $graphLookup and anything the SQL grammar can't express, drop to the driver and map back:
$rows = Article::raw(function ($collection) use ($ids) {
return $collection->aggregate([
['$match' => ['author._id' => ['$in' => $ids]]],
['$group' => ['_id' => '$author._id', 'total' => ['$sum' => 1]]],
]);
});
$models = Article::hydrate($rows->toArray());
Model the document, not the schema. Prefer embedding when the child is always read with the parent and bounded in size (embedsOne / embedsMany), and references when the child is large, unbounded, or independently queried. 5.9 added auto-embedding, which keeps embedded models as first-class Eloquent models with their own casts, events and relations. Use whereHas/eager loading as usual; MorphTo resolves via the foreign key name.
Bulk writes. Model::upsert($rows, ['sku'], ['price', 'stock']), Model::insert($rows) and createOrFirst() map to Mongo's bulk operations and are dramatically cheaper than looping save() in a job or importer.
Reuse the drivers instead of adding infrastructure. Once the connection exists you can move the queue ('driver' => 'mongodb'), cache store, session driver, job batching repository and even a GridFS disk into MongoDB. That's often the highest day-to-day leverage: one datastore, one backup story, no Redis to operate for a small app.
Indexes through migrations. There is no column schema, but the package provides a schema builder, so you can keep index definitions in version control:
Schema::connection('mongodb')->create('articles', function (Blueprint $collection) {
$collection->index('author.id');
$collection->unique('slug');
$collection->index(['tags' => 1, 'published_at' => -1]);
});
Transactions. DB::connection('mongodb')->transaction(fn () => ...) works, but only against a replica set or sharded cluster. Wrap multi-document writes that must be atomic, and keep transaction bodies short.
Schema evolution. For documents that change shape over time, adopt the HasSchemaVersion trait (4.6+) rather than sprinkling ?? null checks through your accessors. Use the DocumentModel trait if you need the package's behaviour on a model that must extend a third-party base class.
lockForUpdate. join() doesn't exist; denormalize or use $lookup in an aggregation. Uniqueness is only real if you create a unique index — validation rules alone are a race condition._id is exposed as a hex string but stored as ObjectId. Route model binding and find() work, but a manually-assigned non-ObjectId _id (e.g. a UUID or slug) needs care: a 24-char hex string gets coerced to ObjectId, so prefer explicit _id types or a separate uuid field. Never set _id => null expecting auto-generation (fixed in 4.4, but older pins bite).where('tags', 'php') matches any document whose tags array contains php, and where('tags', ['php','laravel']) matches an exact array. This is the single most common source of "why did I get more rows than expected".skip/limit. Page 5000 scans 5000 pages. For feeds and exports use range-based pagination (where('_id', '<', $lastId) or created_at cursors) and index the sort field.created_at, and be aware of the timestamp-offset bugs patched across 5.8.x — pin a recent patch.array cast historically JSON-encoded into a string; it's deprecated in favour of real BSON arrays and the new AsBsonDocument / AsBsonArray casts (5.9). Audit existing 'array' casts before upgrading — this changes stored data.$model->update(['address' => [...]]) overwrites the whole embedded document; use dot-notation keys or $model->address->fill([...])->save() to merge.->toMongo() shows the actual filter document; DB::connection('mongodb')->enableQueryLog() plus getQueryLog() prints the real commands; ->explain() tells you whether an index was used. Any query you didn't index shows up as COLLSCAN here first.RefreshDatabase wraps tests in a transaction and will fail on a standalone mongod. Run a single-node replica set (--replSet rs0) in Docker/CI so transactions and the test suite behave like production, and reset collections explicitly.expires_at field only causes eviction if a TTL index exists — create it for the cache, cache-locks and session collections, otherwise they grow forever.prefix for collection names if you share a database across environments, and keep the migration connection distinct in test setups._id), custom casts and the query grammar are all overridable in 5.8/5.9 — reach for a custom Grammar subclass before you patch the package or work around its API.How can I help you explore Laravel packages today?