jarjobs/sonatadtomodelmanager
Provides a clean bridge between Sonata Admin and Model Manager: expose admin-managed entities as model-manager resources, keep configuration in sync, and streamline CRUD workflows. Reuse your existing admin setup while managing models consistently across Symfony/PHP applications.
Start with the reality check: this is not a Laravel package. It is a Symfony bundle that plugs into Sonata Admin's ModelManagerInterface. You can composer require it into a Laravel project and nothing will happen — there is no service provider, no config, no facade, no Laravel-facing API. If your brief assumed "Laravel DTO-to-model package," the correct first step is to rule it out for a Laravel codebase and stop the evaluation there.
You have two legitimate reasons to keep reading:
If case 1 applies, the first fifteen minutes are spent here:
composer.json — the sonata-project/admin-bundle constraint. ^3.x vs ^4.x changes the ModelManagerInterface contract and often the entire viability of the bundle. If it pins SonataCoreBundle, you are on Sonata 3.x.src/DependencyInjection/*Extension.php and src/Resources/config/services.xml|yaml — find the service that implements Sonata\AdminBundle\Model\ModelManagerInterface (and possibly ProxyManagerInterface). That service ID is the only thing you really need.src/ModelManager/ (or equivalent) — read the create() / update() / getNewInstance() implementation. This is where the DTO→entity mapping lives, and where you will debug.First use case: wire the manager to one admin class via the tag, not globally:
app.admin.report:
class: App\Admin\ReportAdmin
tags:
- { name: sonata.admin, manager_type: orm, model_manager: app.admin.model_manager.dto }
Do not replace sonata.admin.manager — everything else in the application will break.
Per-admin manager wiring. The bundle's value proposition is a scoped model_manager on admins where you have already accepted the DTO pattern. Keep the default Doctrine manager everywhere else. This is a per-admin decision, not an application-wide setting.
DTO shape. Define a plain object holding form fields, with no Doctrine annotations/attributes and no persistence identity. Keep it immutable-ish (constructor or fromEntity() factory, explicit setters only for form mapping). The DTO is what Sonata's form binder will hydrate.
Create vs update flows. A typical Admin subclass will:
public function getNewInstance(): object
{
return new ReportDto(); // DTO, not entity
}
protected function prePersist($dto): void
{
$this->getModelManager()->create($dto); // manager maps DTO → entity
}
protected function preUpdate($dto): void
{
$this->getModelManager()->update($dto);
}
The map DTO → entity lives inside the manager; keep the mapping explicit (field-by-field) rather than reflection-based, or you inherit silent drift.
Validation isolation. Add Symfony validation constraints to the DTO, run $form->handleRequest() against the DTO, and let Sonata re-render the form on failure. The manager should never be called on an invalid payload — verify that the hook order is preValidate → validate → prePersist|preUpdate.
Custom field handlers. Multi-entity forms are the strong case: the DTO can hold fields sourced from several entities, and the manager composes them in create()/update(). If the bundle's field-mapping config doesn't cover your case, extend the manager rather than the admin.
Integration testing. Write a functional test that posts the admin form and asserts both the persisted entity and that validation failures leave the DTO intact for re-render. This is the seam where these bundles tend to break.
Laravel translation of the pattern (if that's your goal):
| Sonata piece | Laravel equivalent |
|---|---|
| DTO | app/DataTransferObjects/XxxData (or a Spatie Laravel Data object) |
ModelManagerInterface |
A single-purpose Action (CreateXxx, UpdateXxx) |
prePersist/preUpdate |
Called explicitly by the controller / job |
| Form + validation | FormRequest with rules() |
No dependency needed on the Laravel side; a few hundred lines gets you the whole pattern.
Provenance is the headline risk. Zero stars, zero dependents, single-namespace, no visible CI or release history. Assume abandonment and budget for a fork-and-vendor rather than a Packagist pull. Pin an exact version in composer.json and mirror the source into your repo the moment you rely on it.
Do not replace the global manager. Reassigning sonata.admin.manager breaks every other Sonata admin in the app — services, query proxies, datagrids. Scope the manager per admin via the tag only.
Sonata version coupling is brutal. The ModelManagerInterface signature changed between 3.x and 4.x (nullable return types, ProxyQueryInterface generics, removed methods). A bundle that targets ^3.x will not load in a Sonata 4 admin without patching — and no one upstream is going to patch it for you.
Silent mapping drift. The most common production bug is a field added to the entity but not the DTO (or vice versa), producing partial persistence or lost form input with no error. Mitigate with an explicit field-map test that asserts every DTO field maps to an entity property and vice versa.
Validation-failure lifecycle. On invalid submission, the DTO must survive so Sonata can re-render the form. If your prePersist/preUpdate fires before validation completes, or the DTO is discarded and replaced by an entity on the failed round-trip, you lose user input. Test this path explicitly.
File uploads and nested collections. Verify behavior before committing. These are the two areas where Sonata DTO layers historically break — uploads usually need a Vich/uploadable adapter, and collections need DataMapperInterface attention.
Debuggability tax. There is now one more layer between the form POST and the database write. Every on-call engineer on the admin surface needs to know that prePersist is not the last stop — the model manager is.
License and fork rights. Confirm before adoption. If the license permits it, vendor the code into an internal namespace and add your own CI matrix against your exact Symfony/Sonata/PHP versions. The pattern is the durable asset; the bundle is the disposable part.
Exit plan. Record the exact Sonata Admin version you are coupled to. Treat any Sonata major upgrade as a trigger to re-evaluate — that upgrade will very likely break this bundle, and the answer will be "reimplement the hundred lines in-house" rather than "wait for upstream."
How can I help you explore Laravel packages today?