makasim/yadm-bundle
Symfony bundle that integrates the yadm Yandex Direct API client, providing service definitions and configuration for accessing Yandex Direct advertising campaign data from Symfony applications.
makasim/yadm-bundle is the Symfony glue for yadm (Yet Another Document Mapper), a deliberately minimal MongoDB ODM built around a fast, snapshot-based unit of work and cheap hydration. It is not a Doctrine MongoDB ODM replacement in breadth — it is a replacement in speed for read-heavy, write-light workloads.
Install the bundle and the core library together (the bundle is thin, most of the API lives in makasim/yadm):
composer require makasim/yadm-bundle
Expect to pin or relax constraints yourself — the last tagged release is from 2019, so composer why-not symfony/framework-bundle ^6.0 will tell you immediately if you need a fork or a replace entry.
Register it. Pre-Symfony 4 style is new Yadm\Bundle\YadmBundle() in AppKernel; for 4/5 use config/bundles.php:
Yadm\Bundle\YadmBundle::class => ['all' => true],
Where to look first. Before writing any application code, open two files in vendor/makasim/yadm-bundle:
DependencyInjection/Configuration.php — the authoritative list of config keys (connections, document_managers, mappings). Do not trust blog posts; this file is the contract.Resources/config/*.xml — the service IDs. You will want yadm.registry, and the per-manager yadm.document_manager / yadm.document_manager.<name>. Confirm with bin/console debug:container yadm.Then read vendor/makasim/yadm/src/DocumentManager.php, UnitOfWork.php and Hydrator/ — that is the whole mental model.
Minimal config (shape is Doctrine-like; verify exact key names against Configuration.php):
yadm:
connections:
default:
server: '%env(MONGODB_URL)%'
database: app
document_managers:
default:
connection: default
mappings:
App\Document:
type: annotation
dir: '%kernel.project_dir%/src/Document'
prefix: 'App\Document'
First use case. Do not migrate your write-heavy domain first. Pick a read path — a product listing, a feed, a report — define a plain POPO document, and hydrate it:
$dm = $container->get('yadm.document_manager');
$product = new Product();
$product->name = 'Widget';
$dm->persist($product);
$dm->flush();
$found = $dm->getRepository(Product::class)->find($id);
If that round-trip feels unremarkable, you are using it wrong — the payoff shows up at findBy() over thousands of documents and in hydrate() of raw Mongo cursors.
Custom repositories as the primary API surface. Register a repositoryClass in the document metadata and let controllers/consumers depend on the typed repository instead of the document manager. This is the single biggest lever for keeping yadm out of your controllers:
$repo = $dm->getRepository(Order::class);
$open = $repo->openOrdersForCustomer($customerId); // your method, your query builder
Snapshot-based change tracking. yadm's unit of work diffs the hydrator's original snapshot against the current object state on flush(). That means the idiomatic write pattern is load → mutate setters → flush, with no explicit persist() for managed documents. Use persist() only for genuinely new documents and remove() for deletes. Batch this: load 500, mutate, one flush().
Hydration as a projection tool. The fast path is to bypass the unit of work entirely for read-only endpoints:
$cursor = $dm->getConnection()->selectCollection('app', 'orders')->find($filter);
$orders = $dm->getHydrator(Order::class)->hydrateAll($cursor->toArray());
This skips identity-map bookkeeping and change-set snapshots — often the difference between 40 ms and 8 ms on a large result set. Use it for anything you will not write back.
Multiple document managers for isolation. Configure a second manager pointed at a logging/analytics database and resolve it via yadm.registry rather than hard-coding connections. Keep one manager per bounded context; mixing collections across managers is where identity-map confusion starts.
Integration with Symfony batch surfaces. yadm fits console commands and Messenger handlers well because there is no proxying and no lazy-loading magic. A typical import command:
foreach ($rows as $i => $row) {
$dm->persist($this->map($row));
if ($i % 500 === 0) { $dm->flush(); $dm->clear(); }
}
$dm->flush();
$dm->clear();
Keep documents as POPOs. No base class, no annotations beyond mapping. This means Symfony Serializer, Form types, and your domain layer all work without adapters — one of the few places this package genuinely saves you work versus Doctrine ODM.
Raw Mongo for anything gnarly. Aggregation pipelines, $lookup, bulk writes: drop to $dm->getConnection() / the native MongoDB\Collection. yadm's query builder is thin by design; fighting it is wasted effort.
Abandonment is the headline risk. Last release 2019, 7 stars, 0 dependents. There is no Symfony 6/7 compatibility work, no PHP 8 attribute mapping (annotations only, and doctrine/annotations is deprecated), and no symfony/uid/MongoDB\Driver modernization. Budget for vendoring or forking the bundle — it is small enough (a DI extension plus a handful of XML service definitions) that forking is usually cheaper than fighting it.
Annotations only. If you are on PHP 8 attributes, you will be writing mapping metadata by hand or maintaining a fork. Check whether the core supports a metadata map/array driver before committing; if it does, prefer that over annotations regardless.
Metadata cache is mandatory in production. The hydrator and metadata factory are expensive on cold boot. Point the metadata cache at a real cache pool in prod and clear it whenever mapping changes — stale metadata manifests as silently wrong hydration (missing properties, nulls) rather than an error, which is nasty to debug.
Identity map memory growth. In long-running workers (Messenger, RoadRunner, Swoole), the identity map never shrinks on its own. Call $dm->clear() on a fixed cadence. Symptoms of forgetting: memory creeping monotonically and stale objects being returned from find().
No cascade persist. Unlike Doctrine ODM, embedded documents and references are not auto-persisted. Persist them explicitly or write them as plain arrays on the parent. This trips up everyone coming from Doctrine on day one.
Change detection is snapshot equality. If you mutate an embedded object or array in a way that replaces the reference, fine; if you rely on lazy proxies to notice deep mutations, you will be disappointed. When a flush() appears to do nothing, dump $dm->getUnitOfWork()->getDocumentChangeSet($doc) (verify the method name in UnitOfWork.php) — an empty change set means your snapshot and current state were judged equal.
No lifecycle events, no migrations, no schema tooling. There are no prePersist/postLoad hooks and no --dump-sql equivalent. Use MongoDB's own $jsonSchema collection validators for integrity, and keep index creation in a versioned setup command you control.
No profiler integration. There is no data collector, so slow queries are invisible in the web debug toolbar. Add a thin wrapper around the connection (or a Monolog channel) in dev that logs filter + duration; you will want it within a week.
Extension points worth knowing. Custom hydrators (registered per-document in metadata) let you swap in array-only or partial hydrators for large documents — this is the intended escape hatch for performance tuning. Custom repository factories and a hand-rolled DocumentManager::create() factory are the two other places where the design expects you to intervene rather than configure.
Testing. Point the connection at a throwaway database (app_test) and truncate collections in setUp(). Because there is no schema, "migrations" in tests are just drop(). Do not attempt to mock the document manager — it is a thin object; use a real Mongo instance (or mongodb-memory-server via a test container) and assert on documents.
How can I help you explore Laravel packages today?