Skip to content

Commit dd1b89f

Browse files
authored
feat(laravel): auto configure our tagged interfaces (#7014)
1 parent 500062d commit dd1b89f

File tree

7 files changed

+174
-49
lines changed

7 files changed

+174
-49
lines changed

src/Laravel/ApiPlatformProvider.php

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
use ApiPlatform\Metadata\ResourceClassResolverInterface;
165165
use ApiPlatform\Metadata\UrlGeneratorInterface;
166166
use ApiPlatform\Metadata\Util\Inflector;
167+
use ApiPlatform\Metadata\Util\ReflectionClassRecursiveIterator;
167168
use ApiPlatform\OpenApi\Factory\OpenApiFactory;
168169
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
169170
use ApiPlatform\OpenApi\Options;
@@ -426,25 +427,12 @@ public function register(): void
426427

427428
$this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class);
428429

429-
$this->app->tag([
430-
BooleanFilter::class,
431-
EqualsFilter::class,
432-
PartialSearchFilter::class,
433-
DateFilter::class,
434-
OrderFilter::class,
435-
RangeFilter::class,
436-
SortFilter::class,
437-
SparseFieldset::class,
438-
], EloquentFilterInterface::class);
439-
440430
$this->app->bind(FilterQueryExtension::class, function (Application $app) {
441431
$tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class));
442432

443433
return new FilterQueryExtension(new ServiceLocator($tagged));
444434
});
445435

446-
$this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class);
447-
448436
$this->app->singleton(ItemProvider::class, function (Application $app) {
449437
$tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class));
450438

@@ -455,7 +443,6 @@ public function register(): void
455443

456444
return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged));
457445
});
458-
$this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class);
459446

460447
$this->app->singleton(CallableProvider::class, function (Application $app) {
461448
$tagged = iterator_to_array($app->tagged(ProviderInterface::class));
@@ -488,8 +475,6 @@ public function register(): void
488475
});
489476
}
490477

491-
$this->app->tag([PropertyFilter::class], SerializerFilterInterface::class);
492-
493478
$this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) {
494479
$tagged = iterator_to_array($app->tagged(SerializerFilterInterface::class));
495480

@@ -500,7 +485,6 @@ public function register(): void
500485
$this->app->singleton(SortFilterParameterProvider::class, function (Application $app) {
501486
return new SortFilterParameterProvider();
502487
});
503-
$this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class);
504488

505489
$this->app->singleton('filters', function (Application $app) {
506490
return new ServiceLocator(array_merge(
@@ -540,7 +524,6 @@ public function register(): void
540524

541525
$this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class);
542526

543-
$this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class);
544527
$this->app->singleton(CallableProcessor::class, function (Application $app) {
545528
/** @var ConfigRepository */
546529
$config = $app['config'];
@@ -953,7 +936,7 @@ public function register(): void
953936
});
954937

955938
if (interface_exists(FieldsBuilderEnumInterface::class)) {
956-
$this->registerGraphQl($this->app);
939+
$this->registerGraphQl();
957940
}
958941

959942
$this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) {
@@ -1119,9 +1102,11 @@ function (Application $app) {
11191102
Console\Maker\MakeStateProviderCommand::class,
11201103
]);
11211104
}
1105+
1106+
$this->tagServices();
11221107
}
11231108

1124-
private function registerGraphQl(Application $app): void
1109+
private function registerGraphQl(): void
11251110
{
11261111
$this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) {
11271112
return new GraphQlItemNormalizer(
@@ -1166,7 +1151,7 @@ private function registerGraphQl(Application $app): void
11661151
return new GraphQlHttpExceptionNormalizer();
11671152
});
11681153

1169-
$app->singleton('api_platform.graphql.type_locator', function (Application $app) {
1154+
$this->app->singleton('api_platform.graphql.type_locator', function (Application $app) {
11701155
$tagged = iterator_to_array($app->tagged('api_platform.graphql.type'));
11711156
$services = [];
11721157
foreach ($tagged as $service) {
@@ -1176,20 +1161,21 @@ private function registerGraphQl(Application $app): void
11761161
return new ServiceLocator($services);
11771162
});
11781163

1179-
$app->singleton(TypesFactoryInterface::class, function (Application $app) {
1164+
$this->app->singleton(TypesFactoryInterface::class, function (Application $app) {
11801165
$tagged = iterator_to_array($app->tagged('api_platform.graphql.type'));
11811166

11821167
return new TypesFactory($app->make('api_platform.graphql.type_locator'), array_column($tagged, 'name'));
11831168
});
1184-
$app->singleton(TypesContainerInterface::class, function () {
1169+
1170+
$this->app->singleton(TypesContainerInterface::class, function () {
11851171
return new TypesContainer();
11861172
});
11871173

1188-
$app->singleton(ResourceFieldResolver::class, function (Application $app) {
1174+
$this->app->singleton(ResourceFieldResolver::class, function (Application $app) {
11891175
return new ResourceFieldResolver($app->make(IriConverterInterface::class));
11901176
});
11911177

1192-
$app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) {
1178+
$this->app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) {
11931179
return new TypeBuilder(
11941180
$app->make(TypesContainerInterface::class),
11951181
$app->make(ResourceFieldResolver::class),
@@ -1198,7 +1184,7 @@ private function registerGraphQl(Application $app): void
11981184
);
11991185
});
12001186

1201-
$app->singleton(TypeConverterInterface::class, function (Application $app) {
1187+
$this->app->singleton(TypeConverterInterface::class, function (Application $app) {
12021188
return new TypeConverter(
12031189
$app->make(ContextAwareTypeBuilderInterface::class),
12041190
$app->make(TypesContainerInterface::class),
@@ -1207,11 +1193,11 @@ private function registerGraphQl(Application $app): void
12071193
);
12081194
});
12091195

1210-
$app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) {
1196+
$this->app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) {
12111197
return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class));
12121198
});
12131199

1214-
$app->singleton(GraphQlReadProvider::class, function (Application $app) {
1200+
$this->app->singleton(GraphQlReadProvider::class, function (Application $app) {
12151201
/** @var ConfigRepository */
12161202
$config = $app['config'];
12171203

@@ -1222,9 +1208,9 @@ private function registerGraphQl(Application $app): void
12221208
$config->get('api-platform.graphql.nesting_separator') ?? '__'
12231209
);
12241210
});
1225-
$app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');
1211+
$this->app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read');
12261212

1227-
$app->singleton(ErrorProvider::class, function (Application $app) {
1213+
$this->app->singleton(ErrorProvider::class, function (Application $app) {
12281214
/** @var ConfigRepository */
12291215
$config = $app['config'];
12301216

@@ -1234,9 +1220,8 @@ private function registerGraphQl(Application $app): void
12341220
$app->make(ResourceMetadataCollectionFactoryInterface::class),
12351221
);
12361222
});
1237-
$app->tag([ErrorProvider::class], ProviderInterface::class);
12381223

1239-
$app->singleton(ResolverProvider::class, function (Application $app) {
1224+
$this->app->singleton(ResolverProvider::class, function (Application $app) {
12401225
$resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver'));
12411226
$taggedItemResolvers = iterator_to_array($app->tagged(QueryItemResolverInterface::class));
12421227
$taggedCollectionResolvers = iterator_to_array($app->tagged(QueryCollectionResolverInterface::class));
@@ -1247,19 +1232,19 @@ private function registerGraphQl(Application $app): void
12471232
);
12481233
});
12491234

1250-
$app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver');
1235+
$this->app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver');
12511236

1252-
$app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) {
1237+
$this->app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) {
12531238
return new GraphQlDenormalizeProvider(
12541239
$this->app->make(ResolverProvider::class),
12551240
$app->make(SerializerInterface::class),
12561241
$app->make(GraphQlSerializerContextBuilder::class)
12571242
);
12581243
});
12591244

1260-
$app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize');
1245+
$this->app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize');
12611246

1262-
$app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) {
1247+
$this->app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) {
12631248
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));
12641249
$tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class);
12651250

@@ -1274,34 +1259,34 @@ private function registerGraphQl(Application $app): void
12741259
);
12751260
});
12761261

1277-
$app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) {
1262+
$this->app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) {
12781263
return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class));
12791264
});
12801265

1281-
$app->singleton(NormalizeProcessor::class, function (Application $app) {
1266+
$this->app->singleton(NormalizeProcessor::class, function (Application $app) {
12821267
return new NormalizeProcessor(
12831268
$app->make(SerializerInterface::class),
12841269
$app->make(GraphQlSerializerContextBuilder::class),
12851270
$app->make(Pagination::class)
12861271
);
12871272
});
1288-
$app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize');
1273+
$this->app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize');
12891274

1290-
$app->singleton('api_platform.graphql.state_processor', function (Application $app) {
1275+
$this->app->singleton('api_platform.graphql.state_processor', function (Application $app) {
12911276
return new WriteProcessor(
12921277
$app->make('api_platform.graphql.state_processor.normalize'),
12931278
$app->make(CallableProcessor::class),
12941279
);
12951280
});
12961281

1297-
$app->singleton(ResolverFactoryInterface::class, function (Application $app) {
1282+
$this->app->singleton(ResolverFactoryInterface::class, function (Application $app) {
12981283
return new ResolverFactory(
12991284
$app->make('api_platform.graphql.state_provider.access_checker'),
13001285
$app->make('api_platform.graphql.state_processor')
13011286
);
13021287
});
13031288

1304-
$app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) {
1289+
$this->app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) {
13051290
/** @var ConfigRepository */
13061291
$config = $app['config'];
13071292

@@ -1322,30 +1307,30 @@ private function registerGraphQl(Application $app): void
13221307
);
13231308
});
13241309

1325-
$app->singleton(SchemaBuilderInterface::class, function (Application $app) {
1310+
$this->app->singleton(SchemaBuilderInterface::class, function (Application $app) {
13261311
return new SchemaBuilder($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(TypesFactoryInterface::class), $app->make(TypesContainerInterface::class), $app->make(FieldsBuilderEnumInterface::class));
13271312
});
13281313

1329-
$app->singleton(ErrorHandlerInterface::class, function () {
1314+
$this->app->singleton(ErrorHandlerInterface::class, function () {
13301315
return new GraphQlErrorHandler();
13311316
});
13321317

1333-
$app->singleton(ExecutorInterface::class, function (Application $app) {
1318+
$this->app->singleton(ExecutorInterface::class, function (Application $app) {
13341319
/** @var ConfigRepository */
13351320
$config = $app['config'];
13361321

13371322
return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity') ?? 500, $config->get('api-platform.graphql.max_query_depth') ?? 200);
13381323
});
13391324

1340-
$app->singleton(GraphiQlController::class, function (Application $app) {
1325+
$this->app->singleton(GraphiQlController::class, function (Application $app) {
13411326
/** @var ConfigRepository */
13421327
$config = $app['config'];
13431328
$prefix = $config->get('api-platform.defaults.route_prefix') ?? '';
13441329

13451330
return new GraphiQlController($prefix);
13461331
});
13471332

1348-
$app->singleton(GraphQlEntrypointController::class, function (Application $app) {
1333+
$this->app->singleton(GraphQlEntrypointController::class, function (Application $app) {
13491334
/** @var ConfigRepository */
13501335
$config = $app['config'];
13511336

@@ -1389,4 +1374,54 @@ public function boot(): void
13891374

13901375
$this->loadRoutesFrom(__DIR__.'/routes/api.php');
13911376
}
1377+
1378+
private function tagServices(): void
1379+
{
1380+
$directory = app_path();
1381+
$classes = ReflectionClassRecursiveIterator::getReflectionClassesFromDirectories([$directory]);
1382+
1383+
$this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class);
1384+
$this->autoconfigure($classes, QueryExtensionInterface::class);
1385+
1386+
$this->app->tag([PropertyFilter::class], SerializerFilterInterface::class);
1387+
$this->autoconfigure($classes, SerializerFilterInterface::class);
1388+
1389+
$this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class);
1390+
$this->autoconfigure($classes, ParameterProviderInterface::class);
1391+
1392+
$this->app->tag([
1393+
BooleanFilter::class,
1394+
EqualsFilter::class,
1395+
PartialSearchFilter::class,
1396+
DateFilter::class,
1397+
OrderFilter::class,
1398+
RangeFilter::class,
1399+
SortFilter::class,
1400+
SparseFieldset::class,
1401+
], EloquentFilterInterface::class);
1402+
$this->autoconfigure($classes, EloquentFilterInterface::class);
1403+
1404+
$this->app->tag([ItemProvider::class, CollectionProvider::class, ErrorProvider::class], ProviderInterface::class);
1405+
$this->autoconfigure($classes, ProviderInterface::class);
1406+
$this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class);
1407+
$this->autoconfigure($classes, ProcessorInterface::class);
1408+
}
1409+
1410+
/**
1411+
* @param array<class-string, \ReflectionClass> $classes
1412+
* @param class-string $interface
1413+
*/
1414+
private function autoconfigure(array $classes, string $interface): void
1415+
{
1416+
$m = [];
1417+
foreach ($classes as $className => $refl) {
1418+
if ($refl->implementsInterface($interface)) {
1419+
$m[] = $className;
1420+
}
1421+
}
1422+
1423+
if ($m) {
1424+
$this->app->tag($m, $interface);
1425+
}
1426+
}
13921427
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
namespace ApiPlatform\Laravel\Tests;
15+
16+
use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
17+
use Orchestra\Testbench\Concerns\WithWorkbench;
18+
use Orchestra\Testbench\TestCase;
19+
20+
class AutoconfigureTest extends TestCase
21+
{
22+
use ApiTestAssertionsTrait;
23+
use WithWorkbench;
24+
25+
public function testServiceProvider(): void
26+
{
27+
$response = $this->get('/api/custom_service_provider', headers: ['accept' => ['application/ld+json']]);
28+
$this->assertEquals($response->json()['test'], 'ok');
29+
$response->assertSuccessful();
30+
}
31+
}

src/Laravel/testbench.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
providers:
2+
- Laravel\Tinker\TinkerServiceProvider
23
- ApiPlatform\Laravel\ApiPlatformProvider
34
- Laravel\Sanctum\SanctumServiceProvider
45
- Workbench\App\Providers\WorkbenchServiceProvider
@@ -29,3 +30,5 @@ workbench:
2930
to: app/Models
3031
- from: ./workbench/app/ApiResource/
3132
to: app/ApiResource
33+
- from: ./workbench/app/State/
34+
to: app/State
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
namespace Workbench\App\ApiResource;
15+
16+
use ApiPlatform\Metadata\Get;
17+
use Workbench\App\State\CustomProvider;
18+
19+
#[Get(uriTemplate: 'custom_service_provider', provider: CustomProvider::class)]
20+
class ServiceProvider
21+
{
22+
}

0 commit comments

Comments
 (0)