nilportugues/eloquent-mongodb-repository
A repository layer for Eloquent MongoDB models in Laravel, wrapping query logic behind a clean, testable interface. Handles CRUD, filtering, and pagination so your models stay lean and data access stays consistent.
Check compatibility before you composer require. This package was last released in March 2016 and pins the Laravel 5.x era of illuminate/* plus jenssegers/mongodb (since renamed mongodb/laravel-mongodb). On a modern stack the install will simply fail with a constraint conflict — in that case treat the repo as a reference implementation to read on GitHub rather than a dependency to install.
On a compatible (PHP 7.0/7.1, Laravel 5.4–5.6) stack:
composer require nilportugues/eloquent-mongodb-repository
You also need ext-mongodb (the new extension, not the legacy ext-mongo) and a connection in config/database.php:
'mongodb' => [
'driver' => 'mongodb',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => 27017,
'database' => env('DB_DATABASE'),
],
Where to look first. The package is a thin adapter — two or three classes under vendor/nilportugues/eloquent-mongodb-repository/src. Read those first, then the contracts it implements in vendor/nilportugues/repository/src/.../Contracts (Repository, Filter, FilterCollection, Sort, Order, Page, Pageable, Fields). Those DTOs are the real product; the Mongo class is just translation.
First use case. A read-side endpoint: fetch by id and return a filtered, sorted, paginated list.
// 1. Eloquent model (jenssegers/mongodb)
use Jenssegers\Mongodb\Eloquent\Model;
class User extends Model
{
protected $connection = 'mongodb';
protected $collection = 'users';
protected $fillable = ['name', 'email'];
}
// 2. Repository — set the model and the Mongo primary key
use NilPortugues\Foundation\Infrastructure\Model\Repository\EloquentMongoDB\EloquentRepository;
class UserRepository extends EloquentRepository
{
protected $model = User::class;
protected $primaryKey = '_id'; // not "id"
}
// 3. Bind it
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
Confirm the base class name and constructor signature against vendor/ — the 2016 releases differ slightly on whether the model is injected or resolved from the $model class string.
Interface in the domain, adapter in infrastructure. Declare App\Domain\User\UserRepository yourself (or use the package's Repository contract), and bind the Mongo implementation in a service provider. That is the only reason to accept this dependency: your application code sees Filter/Sort/Pageable value objects, not Eloquent or BSON.
Translate HTTP into DTOs once. Put the query-string parsing in one factory per resource, not in controllers:
// ?name=jo&sort=-created_at&page=2&per_page=25
$filter = new Filter(new FilterCollection([
new Filter('name', new Like('jo')),
]));
$sort = new Sort('created_at', new Order('DESC'));
$pageable = new Pageable(new Page(2, 25), $sort);
$page = $this->users->findAll($pageable); // returns a Page DTO
Exact constructor shapes moved between releases — read the contracts package once and wrap construction in your own helpers so upgrades touch one file.
Keep custom queries out of the generic contract. Mongo-specific needs (regex search, $in batches, aggregation) belong in extra methods on the concrete repository or a narrower interface:
public function activeSince(DateTimeInterface $date): array
{
return $this->model->newQuery()
->where('status', 'active')
->where('created_at', '>', $date)
->get()->all();
}
public function statsByPlan(): array
{
return $this->model->raw(fn ($collection) => $collection->aggregate([
['$group' => ['_id' => '$plan', 'count' => ['$sum' => 1]]],
])->toArray());
}
Map documents to domain entities at the boundary. Don't let Jenssegers\Mongodb\Eloquent\Model leak into services. A small UserMapper::toEntity(array $doc) / toDocument(User $entity) pair keeps save() and find() clean and makes swapping in an in-memory repository trivial.
Test in two layers. Unit-test services against a hand-written in-memory implementation of the same interface (the nilportugues/repository contracts are what make this possible). Integration-test the Mongo adapter against a real MongoDB container — there is no SQLite equivalent here, so mocks will not catch operator translation bugs.
Run both backends if you're migrating. Because the sibling SQL repository shares the contract set, the same service code can be bound to either implementation per environment. That is the package's strongest selling point and the only sane way to justify it.
composer.lock. Do not let it float._id is not id. If $primaryKey stays id, save() will not match the existing document and you will silently insert duplicates. find(Id $id) also needs the raw string form of the ObjectId — cast $model->getKey() to string before wrapping it, and cast incoming request ids to ObjectId explicitly.LIKE may compile to %term% in ways MongoDB cannot express, and BETWEEN/IN/NIN need $gte/$lte/$in. Write a test per operator (eq, neq, gt, gte, lt, lte, in, nin, like, between) against known documents before trusting a single filter in production./term/i) cannot use an index. Normalize fields into lowercase search columns or use Atlas Search/text indexes if the collection grows.Page needs a total, so findAll() runs the query plus a count(). On large collections that count is the expensive part — cache it, or override the total with estimatedDocumentCount().$hidden is serialization, not projection. The base class' $hidden filters array output; it does not restrict fields fetched. Use Fields in the contract or ->project([...]) on the query for real projections.where('created_at', '>', $carbon) matches nothing. Decide the storage type on day one and enforce it with a cast.->enableQueryLog() / ->getQueryLog()), inspect the built query with ->toMongo(), and verify the raw document with DB::connection('mongodb')->getCollection('users')->findOne([...]). Most "the repository is broken" bugs turn out to be a type mismatch visible only in the raw document.findBy(), findAll(), count(), or save() on your concrete repository, and supply a field-mapping class if your entity property names differ from document keys. Wrap save() to fire domain events or validate — the base does neither.illuminate/*, Composer's solver will either fail or silently downgrade your framework. Run composer why-not nilportugues/eloquent-mongodb-repository before you commit to it, and be prepared for the honest verdict: on a new project, use mongodb/laravel-mongodb directly behind your own repository interface and copy the Filter/Sort/Pageable design — not the package — into your codebase.How can I help you explore Laravel packages today?