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

Mongo Odm Aggregation Bundle Laravel Package

solution/mongo-odm-aggregation-bundle

Symfony bundle that adds a fluent aggregation builder for Doctrine MongoDB ODM, making it easier to construct, run, and reuse complex MongoDB aggregation pipelines in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (legacy Symfony 2.x only)

composer require solution/mongo-odm-aggregation-bundle

Register the bundle in app/AppKernel.php:

$bundles = [
    // ...
    new Solution\MongoAggregationBundle\SolutionMongoAggregationBundle(),
];

You must already have doctrine/mongodb-odm-bundle wired up and configured — this bundle does not manage connections, it only adds aggregation on top of an existing ODM setup.

Where to look first

  1. Resources/config/services.xml (or .yml) — this tells you the public service id and its constructor arguments. That is the single most important file; everything below assumes you find the service name there.
  2. DependencyInjection/Configuration.php — any bundle-level config keys (default options like allowDiskUse, cursor settings).
  3. The main aggregation/query class — how it receives the DocumentManager and how it exposes aggregate().

First use case

The canonical first exercise: aggregate a collection you already model with ODM. For example, group orders per customer and sum totals:

$dm = $this->get('doctrine_mongodb')->getManager();
$collection = $dm->getDocumentCollection('AppBundle:Order');

$pipeline = [
    ['$group' => [
        '_id'   => '$customerId',
        'total' => ['$sum' => '$amount'],
        'count' => ['$sum' => 1],
    ]],
    ['$sort' => ['total' => -1]],
    ['$limit' => 10],
];

$results = $service->aggregate($collection, $pipeline)->toArray();

Start with a pipeline that works in the mongo/mongosh shell first, then port it into PHP as a plain array. The bundle is thin over the driver, so the shell is your reference implementation.

Implementation Patterns

Wrap the service, don't call it from controllers

The highest-leverage pattern is to build aggregation into your repositories or a small "report" layer, so controllers stay thin:

class OrderRepository extends DocumentRepository
{
    public function topCustomers(int $limit = 10): array
    {
        return $this->createAggregationBuilder()
            ->group()
                ->field('_id')->expression('$customerId')
                ->field('total')->sum('$amount')
            ->sort('total', 'desc')
            ->limit($limit)
            ->getQuery()
            ->execute()
            ->toArray();
    }
}

Whether the bundle gives you a fluent builder or only a raw pipeline array, keeping the pipeline construction in one repository method per report is what stops this from becoming unmaintainable.

Keep pipelines as named, reusable definitions

Store pipelines in dedicated methods or a PipelineFactory class rather than inlining arrays. Aggregation pipelines are the most brittle part of a Mongo codebase — when a $group key or $lookup shape changes, you want a single place to edit and a clear name to grep for.

Filter early with $match, then group

A common day-to-day pattern: build a $match stage from your existing ODM QueryBuilder criteria, prepend it to the pipeline, and let the database do the filtering before any $group/$lookup. Avoid pulling documents into PHP and grouping there.

Handle $lookup results explicitly

When you join another collection, the joined field is always an array even for a single match. Flatten deliberately:

['$unwind' => ['path' => '$customer', 'preserveNullAndEmptyArrays' => true]],

preserveNullAndEmptyArrays is what keeps orphaned documents in the output — an easy thing to forget when "some rows disappear" in production.

Map to DTOs at the boundary

Aggregation results are plain PHP arrays, not hydrated ODM documents. Convert them to typed value objects (or plain arrays with a documented shape) before returning to the rest of the app, so the rest of your code isn't coupled to raw Mongo result shape.

Test against a real mongod

Unit-mocking the ODM cursor is not worth it — aggregation semantics live in the server. Spin up a throwaway mongod (or use a fixture database) and assert on actual pipeline output. Keep a few "golden pipeline" tests that fail loudly when Mongo changes behaviour across versions.

Integration tips

  • Inject the bundle's service via the container in your repository/service constructor; don't grab it with $this->get() inside deep code.
  • For one-off admin reports, a dedicated ReportService that takes the collection name and pipeline is a clean seam.
  • If you need paging on aggregated output, add $skip/$limit as the final stages — not a LIMIT on a cursor.

Gotchas and Tips

It is Symfony 2-only and effectively unmaintained. Last release 2016, 12 stars, zero dependents. It will not install cleanly on Symfony 3.4+/4+/5+/6+, and its Doctrine ODM dependency constraints pin you to old doctrine/mongodb-odm versions. If you are on anything modern, budget a day to replace it with a thin service that calls Collection::aggregate() directly — the bundle's value is small enough that the replacement is usually cheaper than fighting compatibility. Treat this as a "reference implementation to copy", not a dependency.

Doctrine ODM API drift is the real trap. The AggregationBuilder API (->group()->field()...) and ->aggregate() return types changed between ODM 1.x and later versions. Code samples from this bundle's era (->aggregate() returning a ResultCollection, ->toArray()) may not match your installed version. Pin the ODM version in composer.json and check the installed class names before copying snippets.

Results are arrays, not documents. Don't expect $dm->getRepository(...)->find()-like hydration. If a downstream consumer expects entities, map explicitly. Same for _id fields in $group output — they are the group key, not necessarily an ObjectId.

BSON types bite in $match. Filtering on a customerId stored as ObjectId requires new \MongoId(...) (old driver) / new \MongoDB\BSON\ObjectId(...) (new driver), not the raw string. A string comparison silently returns zero rows. Same with dates: pass \DateTime, not a string.

$group with _id: null is the global aggregate. That's the pattern for a grand total — but be aware it forces a single document and can blow the 16 MB limit; add allowDiskUse and consider a $out/$merge collection for large jobs.

Pass allowDiskUse for anything $sort-heavy or with big $group stages. In-memory sorts cap at 100 MB. On old servers, this is a hard failure, not a slowdown. If the bundle exposes default options, set allowDiskUse: true there so every pipeline benefits.

Cursor batching / memory. ->toArray() on a large result set materialises everything in PHP. For reports, iterate the cursor or add a $limit. Watch BatchSize if the bundle lets you set it.

Debugging a pipeline. The fastest loop is: log the final pipeline array (before sending it), then paste it into mongosh and run db.collection.explain('executionStats').aggregate([...]). If the bundle wraps the ODM cursor, the Doctrine profiler in Symfony's web bar will show the command — enable it in config_dev.yml and confirm the pipeline that was actually sent matches what you intended.

Extension points. If you need shared default stages, override the bundle's main service in your services.yml with a subclass, or add a decorator. There is no plugin architecture to speak of — a decorator around the aggregation service is the cleanest seam.

Version pinning checklist before you commit to this bundle: required Symfony version, required doctrine/mongodb-odm version, PHP version (2016-era code likely has no strict_types, may not run on PHP 8), and whether the driver is ext-mongo (dead) or ext-mongodb. If any of those say "old", plan the extraction now rather than after it becomes load-bearing.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource