Skip to content

perf(laravel): memoize the Symfony RouteCollection in Router#8342

Merged
soyuka merged 1 commit into
api-platform:4.3from
soyuka:fix/laravel-router-memoize-route-collection
Jun 22, 2026
Merged

perf(laravel): memoize the Symfony RouteCollection in Router#8342
soyuka merged 1 commit into
api-platform:4.3from
soyuka:fix/laravel-router-memoize-route-collection

Conversation

@soyuka

@soyuka soyuka commented Jun 22, 2026

Copy link
Copy Markdown
Member
Q A
Branch? 4.3
Bug fix? yes
New feature? no
Deprecations? no
Issues Closes #8337
License MIT

Problem

ApiPlatform\Laravel\Routing\Router::generate() calls getRouteCollection(), which rebuilds the entire Symfony RouteCollection on every call via Illuminate\Routing\AbstractRouteCollection::toSymfonyRouteCollection() — converting the whole Laravel route table each time, with no memoization.

generate() runs once per IRI during normalization (once per relation/sub-resource reference). A single response with several relations triggers the full route-table conversion repeatedly — ~65 times for a resource with ~8 relations in the reporter's Xdebug profiling. php artisan route:cache doesn't help; this conversion happens after Laravel's own route loading.

Fix

Routes are static within a request and Router is registered as a per-request singleton (ApiPlatformProvider), so the built RouteCollection is safely cached on the instance:

public function getRouteCollection(): RouteCollection
{
    if (null !== $this->routeCollection) {
        return $this->routeCollection;
    }

    $routes = $this->router->getRoutes();

    return $this->routeCollection = $routes->toSymfonyRouteCollection();
}

Only the RouteCollection is memoized, not the UrlGenerator — the generator depends on $this->context (mutable via setContext()), while the collection is context-independent and is the expensive part. Memoizing the generator would risk a stale-context bug.

IriConverter and SkolemIriConverter inject the concrete Router directly, so the fix lands in Router itself rather than in a decorator of the interface binding.

SkolemIriConverter typing

As noted in the issue, SkolemIriConverter hard-typed the final concrete Router in its constructor, blocking any userland wrapper (a replacement must stay instanceof Router, impossible for a final class). It only uses $this->router->generate(), so its constructor now type-hints Symfony\Component\Routing\RouterInterface (matching IriConverter), removing the dependency on the concrete class.

Tests

Added Tests/Unit/Routing/RouterTest.php asserting toSymfonyRouteCollection() is invoked once across multiple getRouteCollection() calls and that the same instance is returned. Functional suites (JsonLdTest, JsonApiTest) pass unchanged.

Router::getRouteCollection() rebuilt the entire Symfony RouteCollection
on every call via Illuminate\Routing\AbstractRouteCollection::toSymfonyRouteCollection(),
converting the whole Laravel route table each time. generate() is called
once per IRI during normalization, so a single response with several
relations triggered the full conversion dozens of times (~65 for ~8
relations in profiling).

Routes are static within a request and Router is a per-request singleton,
so the built collection is cached on the instance.

Also type SkolemIriConverter's constructor against
Symfony\Component\Routing\RouterInterface instead of the final concrete
Router, so consumers are no longer pinned to the concrete class.

Closes api-platform#8337
@soyuka soyuka force-pushed the fix/laravel-router-memoize-route-collection branch from 37b86cf to d000a6b Compare June 22, 2026 10:27
@soyuka soyuka merged commit a1abcbc into api-platform:4.3 Jun 22, 2026
108 of 112 checks passed
@soyuka soyuka deleted the fix/laravel-router-memoize-route-collection branch June 22, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant