Skip to content

ReadLinkParameterProvider::getUriVariables() populates every URI variable of the linked operation with the same scalar value #7939

@darthf1

Description

@darthf1

Hi! I ran into an issue with the ReadLinkParameterProvider. Its not working as expected when multiple uri variables are involved. I let Claude generate the rest of the issue text, i hope you dont mind.

API Platform version(s) affected: 4.3.3

Description

ReadLinkParameterProvider::getUriVariables() (in src/State/ParameterProvider/ReadLinkParameterProvider.php) walks every URI variable of the linked operation and assigns the same scalar $value to each key:

foreach ($links as $key => $link) {                                                                                                                                                                                               
    if (!\is_string($key)) {
        $key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty();                                                                                                                         
    }                                  
    if (!$key || !\is_string($key)) {                                                                                                                                                                                             
        continue;
    }                                                                                                                                                                                                                             
 
    $uriVariables[$key] = $value;   // every key gets the same scalar                                                                                                                                                             
}                                      

When the linked operation has a single URI variable (top-level resource like /foos/{id}), the loop runs once and the result is correct. When the linked operation has multiple URI variables (a nested resource like
/foos/{fooId}/bars/{id}), every variable is set to the identifier of the current Link. The downstream ItemProvider then runs a filter where every column is matched against the same value, finds nothing, returns null,
and ReadLinkParameterProvider throws NotFoundHttpException('Relation for link security not found.') — even when the resource exists.

How to reproduce

A nested resource — its primary Get has two URI variables:

#[ApiResource(
    stateOptions: new Options(entityClass: Bar::class),
    operations: [                      
        new Get(                                      
            uriTemplate: '/foos/{fooId}/bars/{id}',
            uriVariables: [                                                                                                                                                                                                       
                'fooId' => new Link(fromClass: FooResource::class, toProperty: 'foo', identifiers: ['id']),
                'id'    => new Link(fromClass: BarResource::class),                                                                                                                                                               
            ],                                        
        ),                              
    ],                                                                                                                                                                                                                            
)]                                                    
class BarResource { /* ... */ }                                                                                                                                                                                                   

Another resource references BarResource with provider: ReadLinkParameterProvider::class:

new Post(
    uriTemplate: '/foos/{fooId}/bars/{barId}/baz',
    uriVariables: [                                   
        'fooId' => new Link(/* ... */),
        'barId' => new Link(                                                                                                                                                                                                      
            fromClass: BarResource::class,
            identifiers: ['id'],                                                                                                                                                                                                  
            provider: ReadLinkParameterProvider::class,   // bug trigger
        ),                                                                                                                                                                                                                        
    ],
)                                                                                                                                                                                                                                 

Request POST /foos/F/bars/B/baz with an existing Bar B under Foo F:

  1. ReadLinkParameterProvider::provide() resolves $linkOperationBarResource's Get.
  2. getUriVariables(B, $barIdLink, $linkOperation) walks BarResource::Get's URI variables and returns ['fooId' => B, 'id' => B] — every key set to the same scalar.
  3. Doctrine ItemProvider runs WHERE foo_id = B AND id = B → no row.
  4. $relation = null; ReadLinkParameterProvider throws NotFoundHttpException('Relation for link security not found.').

Switching BarResource's Get to a single URI variable (/bars/{id}) makes the bug disappear — the loop runs once, id = B, the lookup succeeds. So in practice, this only fires when a Link points to a nested resource whose
primary Get has 2+ URI variables.

Possible Solution

Populate only the URI variable(s) that map to the current Link's fromClass. Pull the rest from the request context ($context['uri_variables']), which holds the actual route values.

private function getUriVariables(mixed $value, Parameter $parameter, Operation $operation, array $context): array                                                                                                                 
{                                      
    // ... unchanged setup ...                                                                                                                                                                                                    
         
    if (\is_array($value)) {                                                                                                                                                                                                      
        return $value;
    }                                                                                                                                                                                                                             
         
    $linkClass = $parameter instanceof Link
        ? ($parameter->getFromClass() ?? $parameter->getToClass())
        : null;                                       

    $contextUriVariables = \is_array($context['uri_variables'] ?? null) ? $context['uri_variables'] : [];                                                                                                                         
                                       
    $uriVariables = [];                                                                                                                                                                                                           
    foreach ($links as $key => $link) {
        // ... key resolution unchanged ...                                                                                                                                                                                       
                                       
        $linkFromClass = $link instanceof Link ? ($link->getFromClass() ?? $link->getToClass()) : null;                                                                                                                           
         
        if (null !== $linkClass && $linkFromClass === $linkClass) {                                                                                                                                                               
            $uriVariables[$key] = $value;
        } elseif (\array_key_exists($key, $contextUriVariables)) {                                                                                                                                                                
            $uriVariables[$key] = $contextUriVariables[$key];
        }                                                                                                                                                                                                                         
    }                                                 
                                                                                                                                                                                                                                  
    return $uriVariables;                                                                                                                                                                                                         
}                                       

$context is already available at both getUriVariables callsites in provide().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions