broutard/mongodb
A Laravel/PHP package that brings MongoDB into your applications, offering query building, model integration, and document handling so you can work with MongoDB collections using familiar Laravel-style syntax.
1. Install the PHP driver first. The package talks to MongoDB through ext-mongodb (the modern driver), not the legacy mongo extension:
pecl install mongodb
# php.ini
extension=mongodb.so
2. Install the package — but verify it first. This is a low-adoption fork of the old Moloquent / jenssegers/laravel-mongodb lineage. Before committing:
composer require broutard/mongodb
Then open vendor/broutard/mongodb/composer.json and check three things: the autoload namespace (forks of this lineage usually keep Jenssegers\Mongodb\*, but some re-namespace to Broutard\Mongodb\*), the illuminate/* version constraints (an old fork won't resolve against Laravel 10/11), and whether a MongodbServiceProvider is listed for auto-discovery. If any of those fail, use mongodb/laravel-mongodb (the official successor) instead — the API below is 95% identical.
3. Register the connection in config/database.php:
'mongodb' => [
'driver' => 'mongodb',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 27017),
'database' => env('DB_DATABASE'),
'username' => env('DB_USERNAME'),
'password' => env('DB_PASSWORD'),
'options' => [
'database' => env('DB_AUTH_DATABASE', 'admin'), // auth source
],
],
4. First use case — a model and a query. Where to look first is the Eloquent/Model.php and Query/Builder.php in the package; they mirror Eloquent closely enough that you can read them like Laravel source.
namespace App\Models;
use Jenssegers\Mongodb\Eloquent\Model; // adjust to the fork's namespace
class Article extends Model
{
protected $connection = 'mongodb';
protected $collection = 'articles';
protected $guarded = [];
protected $dates = ['published_at'];
}
Article::create(['title' => 'Hello', 'status' => 'draft']);
$recent = Article::where('status', 'draft')
->orderBy('published_at', 'desc')
->limit(20)
->get();
Article::where('_id', $id)->push('tags', 'laravel'); // atomic $push, no read-modify-write
Prefer setting 'default' => 'mongodb' in config/database.php only if the whole app is Mongo; otherwise keep mysql/pgsql as default and set $connection per model.
Two entry points, same builder. Use Eloquent models for anything with relations, casts, and events; use the raw collection builder for bulk reads, aggregations, and schema-less documents where you don't want hydration cost:
use Illuminate\Support\Facades\DB;
DB::connection('mongodb')->collection('events')->insert([...]); // no model overhead
DB::connection('mongodb')->collection('events')->where('type', 'click')->count();
Embedding vs. referencing is the core design decision. Embed what you always read together and that has a bounded size; reference anything unbounded or independently queried.
class Post extends Model
{
public function comments() { return $this->embedsMany(Comment::class); } // $post->comments()->create([...])
public function author() { return $this->belongsTo(User::class); } // stores author_id
}
embedsMany is a document array, not a join — you get fast single-document reads and atomic push/pull, but no cross-document whereHas in the SQL sense.
Atomic operators instead of save(). For counters, tags, and array mutations, reach for the operator methods so you avoid lost updates under concurrency:
Post::where('_id', $id)->increment('views');
Post::where('_id', $id)->push('tags', 'php', true); // true = $each
Post::where('_id', $id)->pull('tags', 'php');
Post::where('_id', $id)->unset('legacy_field');
Dot notation for nested fields. Query and update embedded fields directly; don't pull the document into PHP.
Post::where('meta.og.title', 'like', '%Laravel%')->get();
Post::where('_id', $id)->update(['meta.og.title' => 'New title']);
Aggregations go through the raw driver, which is also your escape hatch for anything the builder doesn't wrap:
$cursor = DB::connection('mongodb')->collection('orders')->raw(
fn ($collection) => $collection->aggregate([
['$match' => ['status' => 'paid']],
['$group' => ['_id' => '$customer_id', 'total' => ['$sum' => '$amount']]],
['$sort' => ['total' => -1]],
])
);
foreach ($cursor as $row) { /* stdClass, not a model */ }
Note the return type change: raw() yields driver cursors/arrays, so no model casts, dates, or accessors apply.
Migrations create collections and indexes. The Schema builder is deliberately thin — no column types, since MongoDB is schemaless:
Schema::connection('mongodb')->create('articles', function ($collection) {
$collection->unique('slug');
$collection->index('published_at');
$collection->index(['status' => 1, 'published_at' => -1]);
});
Keep indexes in migrations so environments stay in sync; there is no other source of truth.
Tests need a real MongoDB. RefreshDatabase uses transactional truncation and will not clean Mongo collections. Use Docker in CI and tear down per test:
protected function tearDown(): void
{
Schema::connection('mongodb')->drop('articles');
parent::tearDown();
}
The fork itself is the biggest risk. Zero stars and zero dependents means no one is field-testing it against new Laravel or mongodb driver releases. Pin the version, check the illuminate/* and ext-mongodb constraints, and budget for a switch to mongodb/laravel-mongodb. Namespaces and config keys moved in that migration (Jenssegers\Mongodb → MongoDB\Laravel, ObjectId → MongoDB\BSON\ObjectId).
No transactions on a standalone mongod. Multi-document atomicity requires a replica set (even a single-node one). Model::transaction() silently has nothing to commit against otherwise. Design so each write touches one document.
_id casting is inconsistent. Models set $incrementing = false, $primaryKey = '_id', and cast _id to ObjectId, so $model->id is a string and find($string) works. But on the raw query builder, cast explicitly when a plain string fails to match:
use MongoDB\BSON\ObjectId;
DB::connection('mongodb')->collection('articles')->where('_id', new ObjectId($id))->first();
The 16 MB BSON limit is a hard ceiling. An embedsMany array that grows with user activity (comments, logs, notifications) will eventually fail writes with a document-too-large error. That's the signal to switch to references.
get() on a large collection will exhaust memory. The driver returns a cursor, but Eloquent hydration buffers. Use ->chunk()/->lazy(), or ->cursor() on the raw builder for streaming. Avoid count() on large collections — estimatedDocumentCount() on the raw collection is metadata-only.
like is a regex, not a string match. where('name', 'like', $userInput) interpolates user input into a regex — escape it (preg_quote) or use where('name', 'regex', '/^'.preg_quote($q).'/i'). It's also case-sensitive by default and ignores collation differences you'd expect from MySQL.
Nothing enforces your schema. No unique constraints at the document level beyond indexes you create, and no type coercion. Validators, $casts, and $fillable/$guarded (default it to [] explicitly, or mass assignment will throw) are the only guardrails. A unique index on slug created after duplicates exist will fail to build.
BSON dates are UTC. The driver round-trips MongoDB\BSON\UTCDateTime; declare date fields in $dates/$casts or you'll get raw objects, and remember conversions to app timezone happen at the Eloquent layer only.
Debugging. DB::connection('mongodb')->enableQueryLog() then getQueryLog(), or use the package's debugbar collector if the fork retains it. For anything the builder mangles, drop to the driver:
DB::connection('mongodb')->getMongoClient(); // MongoDB\Client — GridFS, admin commands, sessions
DB::connection('mongodb')->getMongoDB(); // MongoDB\Database — direct collection access
That pair is also the main extension point: register custom casts for domain objects, add Builder macros for repeated aggregation pipelines, and subclass Model to centralize $connection, $guarded, and soft-delete config across the app.
How can I help you explore Laravel packages today?