1. The Core Problem: Mandatory Parameter Persistence
EasyAdmin's AdminUrlGenerator is hardcoded to prioritize entityId. When a developer defines a hierarchical route like this:
#[AdminRoute(path: '/{parentId}/sub-resource/{entityId}')]
class MyCrudController extends AbstractCrudController
{
...
}
the following occurs:
- The Generator Error: When rendering links on the
index or detail pages, the generator sees {parentId} in the route but has no source to fetch it from.
- The Twig Crash: This results in a
MissingMandatoryParametersException during the rendering of standard action buttons (Edit, Delete, etc.).
- The Lifecycle Gap: Even if the URL is manually hacked together, the "Parent" context is lost when moving between
index, new, and edit actions.
2. Proposed Solution: StateProviderInterface
I propose a single, focused interface. Its job is to resolve a specific value for a given parameter name based on the current context (the Request or the specific Entity being rendered in a row).
interface StateProviderInterface
{
/**
* @param string $paramName The placeholder name (e.g., 'parentId')
* @param AdminContext $context The current admin request context
* @return mixed The value to be injected into the route (int, string, or object e.g Uuid::v4())
*/
public function resolve(string $paramName, AdminContext $context): mixed;
}
3. Registration via #[AdminRoute]
The provider is linked directly to the controller via the existing AdminRoute attribute. This creates a clear contract: "If you use this route, use this provider to fill its placeholders (with the exception of entityId)."
#[AdminRoute(
path: '/admin/{parentId}/category/{entityId}',
provider: CategoryStateProvider::class
)]
class CategoryCrudController extends AbstractCrudController
{
// ...
}
Usage Example
class CategoryStateProvider implements StateProviderInterface
{
public function resolve(string $paramName, AdminContext $context): mixed
{
if ($paramName === 'parentId') {
return $context->getEntity()->getInstance()->getParent()->getId();
}
return null; // This should fallback to the default exception for mandatory parameter
}
}
4. How this Resolves the "Top-Level" Logic
A. Bulletproof URL Generation
When the ActionFactory generates a link for an EDIT action:
- It identifies that the route requires
{parentId}.
- It calls the
StateProvider::resolve('parentId', $context).
- The provider returns a value to fill the parentId (e.g.,
$entityDto->getInstance()->getParent()->getId()).
- The URL survives: The generator now has all required data to build a valid Symfony route.
B. Implicit Parent Association
Because the AdminContext now tracks the resolved parentId, standard CRUD methods become aware of the hierarchy:
createEntity(): Can automatically pull parentId from the context to assign the owner.
createIndexQueryBuilder(): Can automatically filter the list so users only see children of the current parentId.
5. Why this "Top-Level" Fix?
Instead of forcing developers to override every single action to manually inject a parentId, this solution fixes the Data Source for the AdminUrlGenerator.
By providing a single entity/value for a given parameter name, we allow EasyAdmin to handle Nested Resources and Multi-tenancy out of the box. It transforms EasyAdmin from a flat-file manager into a true hierarchical CMS.
Comparison: Current vs. Proposed
| Feature |
Current State |
With StateProvider |
| Route Requirement |
Only entityId is auto-filled. |
parentId (or any name) is auto-filled. |
| Implementation |
Overriding 4-5 core methods. |
One Interface + Attribute. |
| Reliability |
Twig crashes on custom routes. |
Twig renders valid links dynamically. |
| Logic Location |
Scattered throughout Controller. |
Centralized in the Provider. |
1. The Core Problem: Mandatory Parameter Persistence
EasyAdmin's
AdminUrlGeneratoris hardcoded to prioritizeentityId. When a developer defines a hierarchical route like this:the following occurs:
indexordetailpages, the generator sees{parentId}in the route but has no source to fetch it from.MissingMandatoryParametersExceptionduring the rendering of standard action buttons (Edit, Delete, etc.).index,new, andeditactions.2. Proposed Solution:
StateProviderInterfaceI propose a single, focused interface. Its job is to resolve a specific value for a given parameter name based on the current context (the Request or the specific Entity being rendered in a row).
3. Registration via
#[AdminRoute]The provider is linked directly to the controller via the existing
AdminRouteattribute. This creates a clear contract: "If you use this route, use this provider to fill its placeholders (with the exception ofentityId)."#[AdminRoute( path: '/admin/{parentId}/category/{entityId}', provider: CategoryStateProvider::class )] class CategoryCrudController extends AbstractCrudController { // ... }Usage Example
4. How this Resolves the "Top-Level" Logic
A. Bulletproof URL Generation
When the
ActionFactorygenerates a link for anEDITaction:{parentId}.StateProvider::resolve('parentId', $context).$entityDto->getInstance()->getParent()->getId()).B. Implicit Parent Association
Because the
AdminContextnow tracks the resolvedparentId, standard CRUD methods become aware of the hierarchy:createEntity(): Can automatically pullparentIdfrom the context to assign the owner.createIndexQueryBuilder(): Can automatically filter the list so users only see children of the currentparentId.5. Why this "Top-Level" Fix?
Instead of forcing developers to override every single action to manually inject a
parentId, this solution fixes the Data Source for theAdminUrlGenerator.By providing a single entity/value for a given parameter name, we allow EasyAdmin to handle Nested Resources and Multi-tenancy out of the box. It transforms EasyAdmin from a flat-file manager into a true hierarchical CMS.
Comparison: Current vs. Proposed
entityIdis auto-filled.parentId(or any name) is auto-filled.