From 88df5a21cacb7bcaef5b733c4aba15238621a490 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 5 Jun 2026 12:08:39 +0200 Subject: [PATCH] fix(laravel): honor path_segment_name_generator config for url segments ApiPlatformProvider hard-coded UnderscorePathSegmentNameGenerator, ignoring any user override. Bind the interface through a closure that reads the api-platform.path_segment_name_generator config key (default UnderscorePathSegmentNameGenerator preserves prior behavior). Allows switching to DashPathSegmentNameGenerator for dasherized segments. Fixes #7244 --- src/Laravel/ApiPlatformProvider.php | 9 ++- .../PathSegmentNameGeneratorDashTest.php | 52 +++++++++++++++++ .../Tests/PathSegmentNameGeneratorTest.php | 58 +++++++++++++++++++ src/Laravel/config/api-platform.php | 9 ++- 4 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 src/Laravel/Tests/PathSegmentNameGeneratorDashTest.php create mode 100644 src/Laravel/Tests/PathSegmentNameGeneratorTest.php diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index 197db3758d..b23bd5b6c7 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -270,7 +270,14 @@ public function register(): void return new SerializerClassMetadataFactory($app->make(ClassMetadataFactoryInterface::class)); }); - $this->app->bind(PathSegmentNameGeneratorInterface::class, UnderscorePathSegmentNameGenerator::class); + $this->app->bind(PathSegmentNameGeneratorInterface::class, static function (Application $app): PathSegmentNameGeneratorInterface { + /** @var ConfigRepository */ + $config = $app['config']; + /** @var class-string $class */ + $class = $config->get('api-platform.path_segment_name_generator') ?? UnderscorePathSegmentNameGenerator::class; + + return $app->make($class); + }); $this->app->singleton(ResourceNameCollectionFactoryInterface::class, static function (Application $app) { /** @var ConfigRepository */ diff --git a/src/Laravel/Tests/PathSegmentNameGeneratorDashTest.php b/src/Laravel/Tests/PathSegmentNameGeneratorDashTest.php new file mode 100644 index 0000000000..0b81db7abd --- /dev/null +++ b/src/Laravel/Tests/PathSegmentNameGeneratorDashTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator; +use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Foundation\Application; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +final class PathSegmentNameGeneratorDashTest extends TestCase +{ + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app->make('config'), static function (Repository $config): void { + $config->set('api-platform.path_segment_name_generator', DashPathSegmentNameGenerator::class); + $config->set('app.debug', true); + }); + } + + public function testConfigOverrideBindingResolvesToDashGenerator(): void + { + $generator = $this->app->make(PathSegmentNameGeneratorInterface::class); + + $this->assertInstanceOf(DashPathSegmentNameGenerator::class, $generator); + } + + public function testRouteUsesDashForMultiWordResource(): void + { + $segments = PathSegmentNameGeneratorTest::collectApiRouteSegments($this->app); + + $this->assertContains('api/product-orders', $segments); + $this->assertNotContains('api/product_orders', $segments); + } +} diff --git a/src/Laravel/Tests/PathSegmentNameGeneratorTest.php b/src/Laravel/Tests/PathSegmentNameGeneratorTest.php new file mode 100644 index 0000000000..a92b7c0a13 --- /dev/null +++ b/src/Laravel/Tests/PathSegmentNameGeneratorTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; +use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Routing\Router; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +final class PathSegmentNameGeneratorTest extends TestCase +{ + use WithWorkbench; + + public function testDefaultBindingResolvesToUnderscoreGenerator(): void + { + $generator = $this->app->make(PathSegmentNameGeneratorInterface::class); + + $this->assertInstanceOf(UnderscorePathSegmentNameGenerator::class, $generator); + } + + public function testDefaultRouteUsesUnderscoreForMultiWordResource(): void + { + $segments = self::collectApiRouteSegments($this->app); + + $this->assertContains('api/product_orders', $segments); + } + + /** + * @return list + */ + public static function collectApiRouteSegments(Application $app): array + { + /** @var Router $router */ + $router = $app->make(Router::class); + + $segments = []; + foreach ($router->getRoutes()->getRoutes() as $route) { + $uri = $route->uri(); + $uri = preg_replace('/\{[^}]+\}/', '', $uri); + $segments[] = rtrim($uri, '/'); + } + + return $segments; + } +} diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index 79b355e0da..b6b1d24707 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -11,6 +11,7 @@ declare(strict_types=1); +use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator; use ApiPlatform\Metadata\UrlGeneratorInterface; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; @@ -154,10 +155,16 @@ // 'openapi' => [ // 'tags' => [], - // ], + // ], 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, + // Class implementing PathSegmentNameGeneratorInterface used to derive route + // segments from resource short names (e.g. `ProductOrder` -> `product_orders`). + // Set to DashPathSegmentNameGenerator::class for dasherized segments + // (e.g. `product-orders`). + 'path_segment_name_generator' => UnderscorePathSegmentNameGenerator::class, + 'serializer' => [ 'hydra_prefix' => false, // 'datetime_format' => \DateTimeInterface::RFC3339,