formapro/yadm
formapro/yadm is a PHP client for the Yandex.Direct advertising API. It offers an object-oriented wrapper for campaigns, ad groups, ads, keywords, and reports, handling authentication, request building, and response parsing so PHP applications can automate Yandex.Direct account management.
Install the package together with the driver and library it sits on:
composer require formapro/yadm mongodb/mongodb
You need ext-mongodb and a running MongoDB. Yadm does not ship a bundle, config tree, or service provider — you wire it yourself, which is the first thing to notice.
Bootstrap a DocumentManager from a MongoDB\Database and a collection→class map:
use Formapro\Yadm\DocumentManager;
$client = new MongoDB\Client(getenv('MONGODB_URI'));
$db = $client->selectDatabase('app');
$dm = DocumentManager::create($db, [
'users' => App\Document\User::class,
'invoices' => App\Document\Invoice::class,
]);
$users = $dm->getStorage(App\Document\User::class);
Where to look first, in order:
Formapro\Yadm\DocumentManager — how the map is built and how storages are resolved.Formapro\Yadm\Storage — the entire CRUD surface (insert, update, delete, find, findOne).Formapro\Yadm\Hydrator — the reflection-compiled hydrator; this is where the speed comes from and where all mapping quirks live.Formapro\Yadm\CollectionFactory — use it directly if you want a Storage without the manager.Your first end-to-end use case should be deliberately boring: a plain PHP class, insert it, read it back.
class User
{
public $_id;
public string $name;
public ?\DateTimeInterface $createdAt = null;
}
$user = new User();
$user->name = 'Ada';
$users->insert($user); // _id is written back onto the object
$found = $users->findOne(['_id' => $user->_id]);
No base class, no annotations, no proxies. That is the whole selling point: fast hydration into objects you already own.
Treat Storage as the low-level primitive, not the API. Wrap each storage in a repository so query details stay out of controllers:
final class UserRepository
{
public function __construct(private Formapro\Yadm\Storage $storage) {}
public function findActive(): iterable
{
return $this->storage->find(
['active' => true],
['sort' => ['createdAt' => -1], 'limit' => 100]
);
}
}
Configure casts on the hydrator. BSON does not round-trip to PHP types for free. \MongoDB\BSON\UTCDateTime stays a BSON object unless you cast it to \DateTimeImmutable; embedded documents arrive as arrays unless you register a nested hydrator. Centralize this per document class (via a HydratorFactory or a small factory method) so every storage for that class behaves identically.
Use projections for read paths. The hydrator only sets what it receives, so a lightweight read model plus ['projection' => ['name' => 1]] is the cheapest way to shrink payloads on list screens. Keep a separate, fuller class for edit screens.
Compose specifications for reusable filters. Formapro\Yadm\Specification lets you express "published in the last 30 days" once and reuse it across repositories instead of copy-pasting filter arrays.
Use the raw collection for anything analytical. $storage->getCollection() hands you the underlying MongoDB\Collection, which is what you want for aggregate(), bulkWrite(), distinct(), and index management. Yadm is a mapper; don't fight it for reporting queries.
Manage indexes out of band. Yadm does not create indexes for you. Put createIndex calls in an idempotent console command or migration so a fresh environment gets the same indexes as production.
Use change streams for side effects. Formapro\Yadm\ChangeStream\Consumer is useful for outbox-style patterns (send email, update search index) without putting that logic on the write path. It requires a replica set.
In a framework, hand-wire it. Register the DocumentManager as a singleton service (built from the client's database plus the map) and register each Storage as a factory. Do not let repositories construct their own clients.
mongodb/mongodb 1.15+. Pin versions deliberately and budget time to vendor a patch rather than assuming upstream fixes.$user->name does nothing until you call $storage->update($user). This is the biggest conceptual shift for anyone coming from Doctrine._id appears only after a successful insert. Code that reads $model->_id before insert() returns gets null. Capture the return/assigned id immediately.Error: must not be accessed before initialization. Default everything to null or provide initializers.__construct or setName() are silently skipped. Validate explicitly after hydration, or use plain DTOs and map to domain objects yourself.DateTimeImmutable and convert at the edges; do not store local times.$in query to avoid N+1.$storage->getCollection()->findOne($filter)->getArrayCopy() next to the hydrated object; the diff tells you instantly whether the problem is a missing cast or bad data. Enable command monitoring on the MongoDB\Client when queries misbehave.Decimal128 come back as different PHP types; a field that is sometimes int and sometimes float will break strict typed properties.Hydrator (or supply a factory) for bespoke casting, and supply your own CollectionFactory to control read preference and write concern — both are worth using instead of patching internals.mongodb-memory-server in CI and reset collections between tests.How can I help you explore Laravel packages today?