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

Sonata Admin Annotation Bundle Laravel Package

ibrows/sonata-admin-annotation-bundle

Configure Sonata Admin classes with PHP annotations instead of verbose YAML or service definitions. Declare fields, filters, list/show/form settings and routes directly above each entity for cleaner, self-documenting admin code. Requires Symfony and SonataAdminBundle.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you install. This bundle has zero stars and zero dependents on the tracked index. Treat it as a reference implementation you may have to fork, not a dependency you can lean on. Before writing a line of code, open the repo and check two things: the composer.json constraints (which Sonata Admin major version it was written against — most likely 2.x/3.x era) and the last commit date. If your project is on Sonata Admin 4/5, expect the mapper signatures to have drifted and budget for patching.

Install and register.

composer require ibrows/sonata-admin-annotation-bundle
// config/bundles.php
Ibrows\Bundle\SonataAdminAnnotationBundle\IbrowsSonataAdminAnnotationBundle::class => ['all' => true],

There is no meaningful configuration to publish — the bundle is essentially an annotation reader plus a mapper-building service. Do not hunt for a large Configuration.php; if there isn't one, that's expected.

Where to look first, in this order:

  1. Annotation/ — the exact annotation class names and their attributes (type, options, label, and whatever ordering/grouping attribute exists). This is the whole API surface.
  2. The service definition(s) in Resources/config/ — find the mapper-builder service ID. That service is the thing your Admin classes will call.
  3. Any Admin/ or Mapper/ directory — this is where the "read annotations off the subject and push them into a FormMapper/ListMapper/ShowMapper/DatagridMapper" logic lives. Read it once; it's small, and knowing it exactly is what saves you hours later.

First use case — convert one mapper on one admin. Take the simplest admin you have (a CRUD with five scalar fields and no relations). Leave configureFormFields() alone. Move only configureListFields() to annotations on the entity:

class Product
{
    /** @Admin\ListField(label="SKU", identifier=true) */
    private $sku;

    /** @Admin\ListField(label="Price") */
    private $price;

    /** @Admin\ListField(label="Active") */
    private $enabled;
}

Then in the admin, replace the hand-written $listMapper->add(...) calls with the bundle's build call (the exact method name comes from step 2 above — commonly something like $this->getMapperBuilder()->build($listMapper, $this->getSubject()) or a trait method). Run the list view. If the columns render, the annotation plumbing is wired correctly and you can expand from there.

Do this on one mapper in one admin first. The failure modes below are much cheaper to diagnose at that scale.

Implementation Patterns

One annotation set per mapper, on the entity. The idiomatic shape is: each mapped property carries up to four annotations — form, list, show, datagrid/filter — and each one's attributes mirror the corresponding $mapper->add($name, $type, $options) signature. type is the Sonata field type string; options is the array you'd normally pass. Think of the annotation as a compressed add() call and the mental model holds up.

The entity as the single source of truth. The real payoff isn't typing less — it's that field metadata lives next to the field. A new developer opening Product.php sees that price is a money form field, a sortable list column, and filterable, without cross-referencing three methods in ProductAdmin. Lean into this: put labels and types on the entity, and reserve the Admin class for things annotations genuinely can't express.

Admin overrides entity. Keep configureFormFields() in the Admin class and call the annotation builder first, then add or adjust specific fields afterwards. This gives you a baseline from annotations plus escape hatches for the two or three fields that need a query_builder, a choices callback, or a custom form type with a service dependency. Never let the annotations be the only path — always keep the imperative code able to run after them.

Hybrid by mapper, not by admin. A good split that stays maintainable:

  • List and Show → annotations. These are almost always flat, static, presentation-only. Highest ratio of boilerplate removed, lowest risk.
  • Datagrid/filters → annotations for the simple ones; imperative for anything needing a custom filter type or join.
  • Form → annotations for scalars; imperative for relations, collections, and embedded forms.

This gets you most of the readability win while keeping the risky 20% in explicit code.

Getters and virtual properties. If the annotation reader walks methods as well as properties, put annotations on getters for computed columns (e.g. getFullName() mapped to a list column). Verify this works — if the reader is property-only via reflection on $reflClass->getProperties(), private parent properties may be skipped.

Shared base entities / inheritance. If several entities extend a common base or use a mapped superclass, annotations on the base class properties are a strong pattern — one @ListField definition, N admins. Test it early, because annotation readers frequently only walk the concrete class's own properties and silently ignore inherited ones.

Migration path in and out. Because the package is unmaintained, keep adoption thin and reversible. Add annotations mapper-by-mapper, commit each one separately, and verify the list/show views after each. If you later need to drop the bundle, each commit is independently revertible back to imperative add() calls. Never let annotations become load-bearing for business logic — they should only ever reproduce what add() would have done.

Integration tip: pin the Sonata version. In composer.json, constrain sonata-project/admin-bundle to the minor version you tested against. An unmaintained annotation reader will not survive a Sonata minor bump that changes mapper method signatures, and you want that to be a deliberate upgrade, not a surprise composer update.

Gotchas and Tips

The options array is the hard ceiling. PHP annotations cannot hold closures, service references, or ::class constants in older doctrine/annotations versions. Anything dynamic — query_builder, choice_loader, entity-type form fields with a custom repository method, constraints with options — has to live in the Admin class. Don't fight this; treat every such field as a signal to keep that mapper imperative.

Annotation caching will lie to you. Doctrine caches parsed annotations (APCu/file in prod). After editing annotations, bin/console cache:clear is not always enough — if APCu is enabled for the CLI-disabled case, you may need to restart PHP-FPM. Symptom: you change a label, nothing happens, you question your sanity. This is the single most common time-waster with annotation-driven config.

Ordering is usually alphabetical by property name, not declaration order. If the bundle has no explicit position/order attribute, your list columns will come out in reflection order, which is not what you wrote. Check for an order/position attribute; if absent, that alone may be a reason to keep configureListFields() imperative. Verify by dumping the resulting mapper.

Silent no-ops. A typo'd annotation name or a missing use statement means the annotation is simply never read — no error, just a missing column. When a field doesn't appear, first confirm the annotation class is actually imported and the reader is being invoked at all (add a temporary dump() in the builder). Second, confirm the property is private/protected and not inherited.

Debugging recipe. After building, inspect what the mapper actually got:

dump($listMapper->getAdmin()->getList());   // or keys() depending on Sonata version
dump($formMapper->getFormBuilder()->all());

Comparing "what I annotated" against "what the mapper holds" localises the bug to either the reader or your annotation in one step.

Extension points, in preference order. (1) Decorate/subclass the mapper-builder service and add your own post-processing — cleanest, no fork. (2) Add your own annotation class alongside the bundle's and extend the reader if it's iterating a known list. (3) Fork. Given zero dependents, a fork is entirely reasonable — but do it as a VCS repository entry in composer.json with a tagged branch, and record the upstream commit you diverged from so you can diff later.

Watch for Sonata API drift on getSubject(). Builders that call $this->getSubject() break on create forms where the subject is null. If the bundle's builder assumes a subject always exists, you'll get a fatal on the "new" action. Guard by constructing a fresh entity (new Product()) before building, or build from the entity class name rather than the subject instance.

Labels and translation. If the bundle passes label straight through, translation works as usual (admin.product.price style keys resolve through the label option). If it auto-derives labels from property names, check whether it applies a translation domain — auto-derived labels that bypass trans are a subtle i18n trap.

Don't annotate id/timestamps. Auto-increment IDs and createdAt/updatedAt are noise in the list view. Explicitly exclude them rather than relying on the bundle's defaults, which may include every mapped property.

Final judgement for the assessment. The idea is genuinely good and the execution cost is low — but with zero stars, zero dependents, and no maintenance signal, this is a pattern to borrow, not a library to depend on for the long haul. Adopt it on list/show mappers where the payoff is highest and the blast radius smallest, keep the Admin classes able to override everything, and treat the bundle as disposable from day one.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib