doctrine/skeleton-mapper
Doctrine Skeleton Mapper is a lightweight mapping library for Doctrine entities, helping convert entity data into simple skeleton objects or arrays for APIs, read models, and presentation layers while keeping persistence concerns separate.
doctrine/skeleton-mapper is not a ready-to-run ORM. It is a skeleton of infrastructure abstractions — built on doctrine/persistence and doctrine/event-manager — for building your own object mapper with Doctrine-style semantics (identity map, change tracking, life-cycle events). Think "Doctrine ORM minus any storage, hydration, or metadata implementation."
Minimal steps:
composer require doctrine/skeleton-mapper and pin the last tag (2024-06-11). It is archived, so treat the version as frozen.Doctrine\SkeletonMapper\ObjectManager — the facade your application talks to.Doctrine\SkeletonMapper\UnitOfWork — identity map, snapshots, changesets, commit.Doctrine\SkeletonMapper\Mapping\ClassMetadata and ClassMetadataFactory — describe your model and its identifiers.Doctrine\SkeletonMapper\Persister\Persister — insert/update/delete against your store.Doctrine\SkeletonMapper\Hydrator\Hydrator — turn raw rows/data into objects and back.Doctrine\SkeletonMapper\Mapping\Driver\MappingDriver — where metadata comes from (attributes, PHP, XML, or hand-written).ObjectManager with: a ClassMetadataFactory (plus a driver), a UnitOfWork (plus event manager and hydrator/persister), and a repository factory.persist / flush / find / remove workflow:$objectManager->persist($order);
$objectManager->flush();
$order = $objectManager->find(Order::class, $id);
$objectManager->remove($order);
$objectManager->flush();
If you want a SQL ORM, use doctrine/orm. This package is the correct choice when you own the storage layer already and only want the object-mapping plumbing.
Custom ObjectManager facade. Subclass or compose Doctrine\SkeletonMapper\ObjectManager and delegate to UnitOfWork. Keep the public surface Doctrine-shaped (find, persist, remove, flush, clear, getRepository, detach, merge) so application code does not learn your internal names.
Persister-per-aggregate. Implement Persister with insert, update, delete, and whatever find shape your repositories need. Keep each persister dumb: convert a changeset row into your storage call, no business logic, no UnitOfWork access. Bulk operations belong in a separate repository method, not in persist.
Hydrator strategy. Start with a reflection-based generic hydrator (property name → column key) to get moving, then hand-write hydrators for hot classes or for objects with value objects, enums, dates, and collections. Register them per-class in the metadata factory / hydrator registry.
Mapping driver choice.
#[Id], #[Column]) — annotations are deprecated.MappingDriver::loadMetadataForClass and read from a config cache.Change tracking via snapshots. Let UnitOfWork capture a snapshot on load/hydrate. On commit, it diffs current state against the snapshot, computes changesets, and hands them to the persister. Call UnitOfWork::computeChangeSet($object) eagerly when your storage round-trip needs to know before flush.
Identity map discipline. One UnitOfWork per request/scope. clear() between batches to avoid unbounded memory growth; detach() for objects you no longer want tracked; never let two UnitOfWorks manage the same instance.
Repositories. Extend Doctrine\SkeletonMapper\ObjectRepository (or implement the interface) for query methods; keep find and findAll going through the identity map, and let everything else go straight to the persister.
Life-cycle events. Register listeners on the Doctrine event manager for prePersist, postPersist, preUpdate, postUpdate, postLoad, preRemove, postRemove. This is where audit timestamps, slugs, and normalization belong — not in the ObjectManager or persisters.
Testing. An in-memory Persister + simple Hydrator is the fastest way to test domain logic without a database. Assert on UnitOfWork::getEntityChangeSet() to prove change detection works for your mapping.
DI wiring. Provide factory classes for: event manager, metadata driver, metadata factory (wrapped in a cache decorator), hydrator registry, persister registry, UnitOfWork, ObjectManager. In Symfony/Laminas, expose ObjectManager as the only injectable service.
Transaction boundary. Because this package does not own your storage, align flush() with your storage transaction (DB transaction, HTTP batch request, queue drain). If the persister can partially fail, implement compensation or re-enqueue; UnitOfWork will not roll itself back.
composer.json to the last release, and if you depend on it long-term, vendor-fork or copy the handful of abstractions you actually use (ObjectManager, UnitOfWork, Persister, Hydrator, ClassMetadata) — they are small.ClassMetadata requires an identifier mapping; a class without one will fail in UnitOfWork. Composite identifiers are supported but make your identity-map key function explicit and consistent.computeChangeSet after mutation.friendsofphp/proxy-manager-lts and make sure hydrators skip uninitialized proxy state.doctrine/annotations is deprecated and will not run on modern PHP without care.doctrine/event-manager 1.x vs 2.x changes listener registration and dispatch signatures. Match the version your skeleton-mapper release expects, or wrap event dispatch in your own small adapter.merge semantics are yours. Do not assume ORM behavior for detached objects; define what merge returns and whether it creates a new tracked instance or re-attaches.getEntityChangeSet(), the scheduled insertions/updates/deletions, and the identity map before blaming the persister. Wrap persister calls in a logger during development to see exactly what SQL/HTTP/storage request is emitted.flush() per entity will be slow against remote stores. Collect changes, flush in batches, and clear() afterward to release snapshots.Persister, Hydrator, MappingDriver, ClassMetadataFactory (decorate with a cache), and ObjectRepository. Everything else is orchestration; those five are where your implementation work concentrates.How can I help you explore Laravel packages today?