Skip to content

Parameter-Specific State Providers for Hierarchical CRUDs #7526

@ucscode

Description

@ucscode

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:

  1. It identifies that the route requires {parentId}.
  2. It calls the StateProvider::resolve('parentId', $context).
  3. The provider returns a value to fill the parentId (e.g., $entityDto->getInstance()->getParent()->getId()).
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions