Skip to content

Commit 5124d8c

Browse files
authored
fix(laravel): Prevent overwriting existing routes on the router (#6941)
* Prevent overwriting existing routes on the router * Move routes to its own routes file and add domain support * Fix tests and stan. Run cs fixer * Revert laravel pint changes
1 parent af35d34 commit 5124d8c

File tree

4 files changed

+141
-106
lines changed

4 files changed

+141
-106
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 2 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer;
6767
use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer;
6868
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
69-
use ApiPlatform\JsonLd\Action\ContextAction;
7069
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
7170
use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder;
7271
use ApiPlatform\JsonLd\ContextBuilderInterface;
@@ -124,7 +123,6 @@
124123
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
125124
use ApiPlatform\Laravel\State\SwaggerUiProvider;
126125
use ApiPlatform\Laravel\State\ValidateProvider;
127-
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
128126
use ApiPlatform\Metadata\IdentifiersExtractor;
129127
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
130128
use ApiPlatform\Metadata\InflectorInterface;
@@ -196,10 +194,6 @@
196194
use Illuminate\Config\Repository as ConfigRepository;
197195
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface;
198196
use Illuminate\Contracts\Foundation\Application;
199-
use Illuminate\Contracts\Foundation\CachesRoutes;
200-
use Illuminate\Http\Request;
201-
use Illuminate\Routing\Route;
202-
use Illuminate\Routing\RouteCollection;
203197
use Illuminate\Routing\Router;
204198
use Illuminate\Support\ServiceProvider;
205199
use Negotiation\Negotiator;
@@ -1346,7 +1340,7 @@ private function registerGraphQl(Application $app): void
13461340
/**
13471341
* Bootstrap services.
13481342
*/
1349-
public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, Router $router): void
1343+
public function boot(): void
13501344
{
13511345
if ($this->app->runningInConsole()) {
13521346
$this->publishes([
@@ -1368,94 +1362,6 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
13681362
$typeBuilder->setFieldsBuilderLocator(new ServiceLocator(['api_platform.graphql.fields_builder' => $fieldsBuilder]));
13691363
}
13701364

1371-
if (!$this->shouldRegisterRoutes()) {
1372-
return;
1373-
}
1374-
1375-
$globalMiddlewares = $config->get('api-platform.routes.middleware');
1376-
$routeCollection = new RouteCollection();
1377-
foreach ($resourceNameCollectionFactory->create() as $resourceClass) {
1378-
foreach ($resourceMetadataFactory->create($resourceClass) as $resourceMetadata) {
1379-
foreach ($resourceMetadata->getOperations() as $operation) {
1380-
$uriTemplate = $operation->getUriTemplate();
1381-
// _format is read by the middleware
1382-
$uriTemplate = $operation->getRoutePrefix().str_replace('{._format}', '{_format?}', $uriTemplate);
1383-
$route = (new Route([$operation->getMethod()], $uriTemplate, [ApiPlatformController::class, '__invoke']))
1384-
->where('_format', '^\.[a-zA-Z]+')
1385-
->name($operation->getName())
1386-
->setDefaults(['_api_operation_name' => $operation->getName(), '_api_resource_class' => $operation->getClass()]);
1387-
1388-
$route->middleware(ApiPlatformMiddleware::class.':'.$operation->getName());
1389-
$route->middleware($globalMiddlewares);
1390-
$route->middleware($operation->getMiddleware());
1391-
1392-
$routeCollection->add($route);
1393-
}
1394-
}
1395-
}
1396-
1397-
$prefix = $config->get('api-platform.defaults.route_prefix') ?? '';
1398-
$route = new Route(['GET'], $prefix.'/contexts/{shortName?}{_format?}', [ContextAction::class, '__invoke']);
1399-
$route->name('api_jsonld_context');
1400-
$route->middleware(ApiPlatformMiddleware::class);
1401-
$route->middleware($globalMiddlewares);
1402-
$routeCollection->add($route);
1403-
$route = new Route(['GET'], $prefix.'/docs{_format?}', function (Request $request, Application $app) {
1404-
$documentationAction = $app->make(DocumentationController::class);
1405-
1406-
return $documentationAction->__invoke($request);
1407-
});
1408-
$route->name('api_doc');
1409-
$route->middleware(ApiPlatformMiddleware::class);
1410-
$route->middleware($globalMiddlewares);
1411-
$routeCollection->add($route);
1412-
1413-
$route = new Route(['GET'], $prefix.'/.well-known/genid/{id}', function (): void {
1414-
throw new NotExposedHttpException('This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.');
1415-
});
1416-
$route->name('api_genid');
1417-
$route->middleware(ApiPlatformMiddleware::class);
1418-
$route->middleware($globalMiddlewares);
1419-
$routeCollection->add($route);
1420-
1421-
if ($config->get('api-platform.graphql.enabled')) {
1422-
$route = new Route(['POST', 'GET'], $prefix.'/graphql', function (Application $app, Request $request) {
1423-
$entrypointAction = $app->make(GraphQlEntrypointController::class);
1424-
1425-
return $entrypointAction->__invoke($request);
1426-
});
1427-
$route->middleware($globalMiddlewares);
1428-
$routeCollection->add($route);
1429-
1430-
$route = new Route(['GET'], $prefix.'/graphiql', function (Application $app) {
1431-
$controller = $app->make(GraphiQlController::class);
1432-
1433-
return $controller->__invoke();
1434-
});
1435-
$route->middleware($globalMiddlewares);
1436-
$routeCollection->add($route);
1437-
}
1438-
1439-
$route = new Route(['GET'], $prefix.'/{index?}{_format?}', function (Request $request, Application $app) {
1440-
$entrypointAction = $app->make(EntrypointController::class);
1441-
1442-
return $entrypointAction->__invoke($request);
1443-
});
1444-
$route->where('index', 'index');
1445-
$route->name('api_entrypoint');
1446-
$route->middleware(ApiPlatformMiddleware::class);
1447-
$route->middleware($globalMiddlewares);
1448-
$routeCollection->add($route);
1449-
1450-
$router->setRoutes($routeCollection);
1451-
}
1452-
1453-
private function shouldRegisterRoutes(): bool
1454-
{
1455-
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
1456-
return false;
1457-
}
1458-
1459-
return true;
1365+
$this->loadRoutesFrom(__DIR__.'/routes/api.php');
14601366
}
14611367
}

src/Laravel/Tests/ApiTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
15+
use Illuminate\Contracts\Config\Repository;
16+
use Illuminate\Foundation\Application;
17+
use Illuminate\Foundation\Testing\RefreshDatabase;
18+
use Orchestra\Testbench\Concerns\WithWorkbench;
19+
use Orchestra\Testbench\TestCase;
20+
21+
class ApiTest extends TestCase
22+
{
23+
use ApiTestAssertionsTrait;
24+
use RefreshDatabase;
25+
use WithWorkbench;
26+
27+
/**
28+
* @param Application $app
29+
*/
30+
protected function defineEnvironment($app): void
31+
{
32+
tap($app['config'], function (Repository $config): void {
33+
$config->set('api-platform.routes.domain', 'http://test.com');
34+
$config->set('app.debug', true);
35+
$config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]);
36+
$config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]);
37+
});
38+
}
39+
40+
public function testDomainCanBeSet(): void
41+
{
42+
$response = $this->get('http://foobar.com/api/', ['accept' => ['application/ld+json']]);
43+
$response->assertNotFound();
44+
45+
$response = $this->get('http://test.com/api/', ['accept' => ['application/ld+json']]);
46+
$response->assertSuccessful();
47+
}
48+
}

src/Laravel/config/api-platform.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'version' => '1.0.0',
1111

1212
'routes' => [
13+
'domain' => null,
1314
// Global middleware applied to every API Platform routes
1415
// 'middleware' => []
1516
],
@@ -43,20 +44,20 @@
4344
'pagination_enabled' => true,
4445
'pagination_partial' => false,
4546
'pagination_client_enabled' => false,
46-
'pagination_client_items_per_page' => false,
47-
'pagination_client_partial' => false,
48-
'pagination_items_per_page' => 30,
49-
'pagination_maximum_items_per_page' => 30,
47+
'pagination_client_items_per_page' => false,
48+
'pagination_client_partial' => false,
49+
'pagination_items_per_page' => 30,
50+
'pagination_maximum_items_per_page' => 30,
5051
'route_prefix' => '/api',
5152
'middleware' => [],
5253
],
5354

54-
'pagination' => [
55-
'page_parameter_name' => 'page',
56-
'enabled_parameter_name' => 'pagination',
57-
'items_per_page_parameter_name' => 'itemsPerPage',
58-
'partial_parameter_name' => 'partial',
59-
],
55+
'pagination' => [
56+
'page_parameter_name' => 'page',
57+
'enabled_parameter_name' => 'pagination',
58+
'items_per_page_parameter_name' => 'itemsPerPage',
59+
'partial_parameter_name' => 'partial',
60+
],
6061

6162
'graphql' => [
6263
'enabled' => false,

src/Laravel/routes/api.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
use ApiPlatform\JsonLd\Action\ContextAction;
15+
use ApiPlatform\Laravel\ApiPlatformMiddleware;
16+
use ApiPlatform\Laravel\Controller\ApiPlatformController;
17+
use ApiPlatform\Laravel\Controller\DocumentationController;
18+
use ApiPlatform\Laravel\Controller\EntrypointController;
19+
use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController;
20+
use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController;
21+
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
22+
use ApiPlatform\Metadata\HttpOperation;
23+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
24+
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
25+
use Illuminate\Support\Facades\Route;
26+
use Illuminate\Support\Str;
27+
28+
$globalMiddlewares = config()->get('api-platform.routes.middleware', []);
29+
$domain = config()->get('api-platform.routes.domain');
30+
31+
Route::domain($domain)->middleware($globalMiddlewares)->group(function (): void {
32+
$resourceNameCollectionFactory = app()->make(ResourceNameCollectionFactoryInterface::class);
33+
$resourceMetadataFactory = app()->make(ResourceMetadataCollectionFactoryInterface::class);
34+
35+
foreach ($resourceNameCollectionFactory->create() as $resourceClass) {
36+
foreach ($resourceMetadataFactory->create($resourceClass) as $resourceMetadata) {
37+
foreach ($resourceMetadata->getOperations() as $operation) {
38+
/* @var HttpOperation $operation */
39+
Route::addRoute($operation->getMethod(), Str::replace('{._format}', '{_format?}', $operation->getUriTemplate()), ApiPlatformController::class)
40+
->prefix($operation->getRoutePrefix())
41+
->middleware(ApiPlatformMiddleware::class.':'.$operation->getName())
42+
->middleware($operation->getMiddleware())
43+
->where('_format', '^\.[a-zA-Z]+')
44+
->name($operation->getName())
45+
->setDefaults(['_api_operation_name' => $operation->getName(), '_api_resource_class' => $operation->getClass()]);
46+
}
47+
}
48+
}
49+
50+
$prefix = config()->get('api-platform.defaults.route_prefix') ?? '';
51+
52+
Route::group(['prefix' => $prefix], function (): void {
53+
Route::group(['middleware' => ApiPlatformMiddleware::class], function (): void {
54+
Route::get('/contexts/{shortName?}{_format?}', ContextAction::class)
55+
->middleware(ApiPlatformMiddleware::class)
56+
->name('api_jsonld_context');
57+
58+
Route::get('/docs{_format?}', DocumentationController::class)
59+
->middleware(ApiPlatformMiddleware::class)
60+
->name('api_doc');
61+
62+
Route::get('/.well-known/genid/{id}', fn () => throw new NotExposedHttpException('This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.'))
63+
->middleware(ApiPlatformMiddleware::class)
64+
->name('api_genid');
65+
66+
Route::get('/{index?}{_format?}', EntrypointController::class)
67+
->where('index', 'index')
68+
->middleware(ApiPlatformMiddleware::class)
69+
->name('api_entrypoint');
70+
});
71+
72+
if (config()->get('api-platform.graphql.enabled')) {
73+
Route::addRoute(['POST', 'GET'], '/graphql', GraphQlEntrypointController::class)
74+
->name('api_graphql');
75+
76+
Route::get('/graphiql', GraphiQlController::class)
77+
->name('api_graphiql');
78+
}
79+
});
80+
});

0 commit comments

Comments
 (0)