diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index cc53e58be8b..47641e0b6f1 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -164,6 +164,7 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\Inflector; +use ApiPlatform\Metadata\Util\ReflectionClassRecursiveIterator; use ApiPlatform\OpenApi\Factory\OpenApiFactory; use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; use ApiPlatform\OpenApi\Options; @@ -426,25 +427,12 @@ public function register(): void $this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class); - $this->app->tag([ - BooleanFilter::class, - EqualsFilter::class, - PartialSearchFilter::class, - DateFilter::class, - OrderFilter::class, - RangeFilter::class, - SortFilter::class, - SparseFieldset::class, - ], EloquentFilterInterface::class); - $this->app->bind(FilterQueryExtension::class, function (Application $app) { $tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class)); return new FilterQueryExtension(new ServiceLocator($tagged)); }); - $this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class); - $this->app->singleton(ItemProvider::class, function (Application $app) { $tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class)); @@ -455,7 +443,6 @@ public function register(): void return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged)); }); - $this->app->tag([ItemProvider::class, CollectionProvider::class], ProviderInterface::class); $this->app->singleton(CallableProvider::class, function (Application $app) { $tagged = iterator_to_array($app->tagged(ProviderInterface::class)); @@ -488,8 +475,6 @@ public function register(): void }); } - $this->app->tag([PropertyFilter::class], SerializerFilterInterface::class); - $this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) { $tagged = iterator_to_array($app->tagged(SerializerFilterInterface::class)); @@ -500,7 +485,6 @@ public function register(): void $this->app->singleton(SortFilterParameterProvider::class, function (Application $app) { return new SortFilterParameterProvider(); }); - $this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class); $this->app->singleton('filters', function (Application $app) { return new ServiceLocator(array_merge( @@ -540,7 +524,6 @@ public function register(): void $this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class); - $this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class); $this->app->singleton(CallableProcessor::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -953,7 +936,7 @@ public function register(): void }); if (interface_exists(FieldsBuilderEnumInterface::class)) { - $this->registerGraphQl($this->app); + $this->registerGraphQl(); } $this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) { @@ -1119,9 +1102,11 @@ function (Application $app) { Console\Maker\MakeStateProviderCommand::class, ]); } + + $this->tagServices(); } - private function registerGraphQl(Application $app): void + private function registerGraphQl(): void { $this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) { return new GraphQlItemNormalizer( @@ -1166,7 +1151,7 @@ private function registerGraphQl(Application $app): void return new GraphQlHttpExceptionNormalizer(); }); - $app->singleton('api_platform.graphql.type_locator', function (Application $app) { + $this->app->singleton('api_platform.graphql.type_locator', function (Application $app) { $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); $services = []; foreach ($tagged as $service) { @@ -1176,20 +1161,21 @@ private function registerGraphQl(Application $app): void return new ServiceLocator($services); }); - $app->singleton(TypesFactoryInterface::class, function (Application $app) { + $this->app->singleton(TypesFactoryInterface::class, function (Application $app) { $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); return new TypesFactory($app->make('api_platform.graphql.type_locator'), array_column($tagged, 'name')); }); - $app->singleton(TypesContainerInterface::class, function () { + + $this->app->singleton(TypesContainerInterface::class, function () { return new TypesContainer(); }); - $app->singleton(ResourceFieldResolver::class, function (Application $app) { + $this->app->singleton(ResourceFieldResolver::class, function (Application $app) { return new ResourceFieldResolver($app->make(IriConverterInterface::class)); }); - $app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) { + $this->app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) { return new TypeBuilder( $app->make(TypesContainerInterface::class), $app->make(ResourceFieldResolver::class), @@ -1198,7 +1184,7 @@ private function registerGraphQl(Application $app): void ); }); - $app->singleton(TypeConverterInterface::class, function (Application $app) { + $this->app->singleton(TypeConverterInterface::class, function (Application $app) { return new TypeConverter( $app->make(ContextAwareTypeBuilderInterface::class), $app->make(TypesContainerInterface::class), @@ -1207,11 +1193,11 @@ private function registerGraphQl(Application $app): void ); }); - $app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) { + $this->app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) { return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class)); }); - $app->singleton(GraphQlReadProvider::class, function (Application $app) { + $this->app->singleton(GraphQlReadProvider::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -1222,9 +1208,9 @@ private function registerGraphQl(Application $app): void $config->get('api-platform.graphql.nesting_separator') ?? '__' ); }); - $app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read'); + $this->app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read'); - $app->singleton(ErrorProvider::class, function (Application $app) { + $this->app->singleton(ErrorProvider::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -1234,9 +1220,8 @@ private function registerGraphQl(Application $app): void $app->make(ResourceMetadataCollectionFactoryInterface::class), ); }); - $app->tag([ErrorProvider::class], ProviderInterface::class); - $app->singleton(ResolverProvider::class, function (Application $app) { + $this->app->singleton(ResolverProvider::class, function (Application $app) { $resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver')); $taggedItemResolvers = iterator_to_array($app->tagged(QueryItemResolverInterface::class)); $taggedCollectionResolvers = iterator_to_array($app->tagged(QueryCollectionResolverInterface::class)); @@ -1247,9 +1232,9 @@ private function registerGraphQl(Application $app): void ); }); - $app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver'); + $this->app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver'); - $app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) { + $this->app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) { return new GraphQlDenormalizeProvider( $this->app->make(ResolverProvider::class), $app->make(SerializerInterface::class), @@ -1257,9 +1242,9 @@ private function registerGraphQl(Application $app): void ); }); - $app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize'); + $this->app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize'); - $app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) { + $this->app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) { $tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class)); $tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class); @@ -1274,34 +1259,34 @@ private function registerGraphQl(Application $app): void ); }); - $app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) { + $this->app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) { return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class)); }); - $app->singleton(NormalizeProcessor::class, function (Application $app) { + $this->app->singleton(NormalizeProcessor::class, function (Application $app) { return new NormalizeProcessor( $app->make(SerializerInterface::class), $app->make(GraphQlSerializerContextBuilder::class), $app->make(Pagination::class) ); }); - $app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize'); + $this->app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize'); - $app->singleton('api_platform.graphql.state_processor', function (Application $app) { + $this->app->singleton('api_platform.graphql.state_processor', function (Application $app) { return new WriteProcessor( $app->make('api_platform.graphql.state_processor.normalize'), $app->make(CallableProcessor::class), ); }); - $app->singleton(ResolverFactoryInterface::class, function (Application $app) { + $this->app->singleton(ResolverFactoryInterface::class, function (Application $app) { return new ResolverFactory( $app->make('api_platform.graphql.state_provider.access_checker'), $app->make('api_platform.graphql.state_processor') ); }); - $app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) { + $this->app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -1322,22 +1307,22 @@ private function registerGraphQl(Application $app): void ); }); - $app->singleton(SchemaBuilderInterface::class, function (Application $app) { + $this->app->singleton(SchemaBuilderInterface::class, function (Application $app) { return new SchemaBuilder($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(TypesFactoryInterface::class), $app->make(TypesContainerInterface::class), $app->make(FieldsBuilderEnumInterface::class)); }); - $app->singleton(ErrorHandlerInterface::class, function () { + $this->app->singleton(ErrorHandlerInterface::class, function () { return new GraphQlErrorHandler(); }); - $app->singleton(ExecutorInterface::class, function (Application $app) { + $this->app->singleton(ExecutorInterface::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; 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); }); - $app->singleton(GraphiQlController::class, function (Application $app) { + $this->app->singleton(GraphiQlController::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; $prefix = $config->get('api-platform.defaults.route_prefix') ?? ''; @@ -1345,7 +1330,7 @@ private function registerGraphQl(Application $app): void return new GraphiQlController($prefix); }); - $app->singleton(GraphQlEntrypointController::class, function (Application $app) { + $this->app->singleton(GraphQlEntrypointController::class, function (Application $app) { /** @var ConfigRepository */ $config = $app['config']; @@ -1389,4 +1374,54 @@ public function boot(): void $this->loadRoutesFrom(__DIR__.'/routes/api.php'); } + + private function tagServices(): void + { + $directory = app_path(); + $classes = ReflectionClassRecursiveIterator::getReflectionClassesFromDirectories([$directory]); + + $this->app->tag([FilterQueryExtension::class], QueryExtensionInterface::class); + $this->autoconfigure($classes, QueryExtensionInterface::class); + + $this->app->tag([PropertyFilter::class], SerializerFilterInterface::class); + $this->autoconfigure($classes, SerializerFilterInterface::class); + + $this->app->tag([SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class], ParameterProviderInterface::class); + $this->autoconfigure($classes, ParameterProviderInterface::class); + + $this->app->tag([ + BooleanFilter::class, + EqualsFilter::class, + PartialSearchFilter::class, + DateFilter::class, + OrderFilter::class, + RangeFilter::class, + SortFilter::class, + SparseFieldset::class, + ], EloquentFilterInterface::class); + $this->autoconfigure($classes, EloquentFilterInterface::class); + + $this->app->tag([ItemProvider::class, CollectionProvider::class, ErrorProvider::class], ProviderInterface::class); + $this->autoconfigure($classes, ProviderInterface::class); + $this->app->tag([RemoveProcessor::class, PersistProcessor::class], ProcessorInterface::class); + $this->autoconfigure($classes, ProcessorInterface::class); + } + + /** + * @param array $classes + * @param class-string $interface + */ + private function autoconfigure(array $classes, string $interface): void + { + $m = []; + foreach ($classes as $className => $refl) { + if ($refl->implementsInterface($interface)) { + $m[] = $className; + } + } + + if ($m) { + $this->app->tag($m, $interface); + } + } } diff --git a/src/Laravel/Tests/AutoconfigureTest.php b/src/Laravel/Tests/AutoconfigureTest.php new file mode 100644 index 00000000000..5cae1f2129b --- /dev/null +++ b/src/Laravel/Tests/AutoconfigureTest.php @@ -0,0 +1,31 @@ + + * + * 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\Laravel\Test\ApiTestAssertionsTrait; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class AutoconfigureTest extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + public function testServiceProvider(): void + { + $response = $this->get('/api/custom_service_provider', headers: ['accept' => ['application/ld+json']]); + $this->assertEquals($response->json()['test'], 'ok'); + $response->assertSuccessful(); + } +} diff --git a/src/Laravel/testbench.yaml b/src/Laravel/testbench.yaml index eb72b659cf8..4477a67ec42 100644 --- a/src/Laravel/testbench.yaml +++ b/src/Laravel/testbench.yaml @@ -1,4 +1,5 @@ providers: + - Laravel\Tinker\TinkerServiceProvider - ApiPlatform\Laravel\ApiPlatformProvider - Laravel\Sanctum\SanctumServiceProvider - Workbench\App\Providers\WorkbenchServiceProvider @@ -29,3 +30,5 @@ workbench: to: app/Models - from: ./workbench/app/ApiResource/ to: app/ApiResource + - from: ./workbench/app/State/ + to: app/State diff --git a/src/Laravel/workbench/app/ApiResource/ServiceProvider.php b/src/Laravel/workbench/app/ApiResource/ServiceProvider.php new file mode 100644 index 00000000000..c339a55a869 --- /dev/null +++ b/src/Laravel/workbench/app/ApiResource/ServiceProvider.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\ApiResource; + +use ApiPlatform\Metadata\Get; +use Workbench\App\State\CustomProvider; + +#[Get(uriTemplate: 'custom_service_provider', provider: CustomProvider::class)] +class ServiceProvider +{ +} diff --git a/src/Laravel/workbench/app/Models/PrefixedOperation.php b/src/Laravel/workbench/app/Models/PrefixedOperation.php index e3d13300408..9c02bf02532 100644 --- a/src/Laravel/workbench/app/Models/PrefixedOperation.php +++ b/src/Laravel/workbench/app/Models/PrefixedOperation.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Laravel\workbench\app\Models; +namespace Workbench\App\Models; use ApiPlatform\Metadata\Post; use Illuminate\Database\Eloquent\Factories\HasFactory; diff --git a/src/Laravel/workbench/app/State/CustomProvider.php b/src/Laravel/workbench/app/State/CustomProvider.php new file mode 100644 index 00000000000..0da4d74c7e7 --- /dev/null +++ b/src/Laravel/workbench/app/State/CustomProvider.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Workbench\App\ApiResource\ServiceProvider; + +/** + * @implements ProviderInterface + */ +class CustomProvider implements ProviderInterface +{ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): \stdClass + { + $s = new \stdClass(); + $s->test = 'ok'; + + return $s; + } +} diff --git a/src/Metadata/Util/ReflectionClassRecursiveIterator.php b/src/Metadata/Util/ReflectionClassRecursiveIterator.php index 76d99be2090..41de473d4ad 100644 --- a/src/Metadata/Util/ReflectionClassRecursiveIterator.php +++ b/src/Metadata/Util/ReflectionClassRecursiveIterator.php @@ -32,6 +32,8 @@ private function __construct() } /** + * @param string[] $directories + * * @return array */ public static function getReflectionClassesFromDirectories(array $directories): array @@ -45,7 +47,7 @@ public static function getReflectionClassesFromDirectories(array $directories): foreach ($directories as $path) { $iterator = new \RegexIterator( new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ), '/^.+\.php$/i',