diff --git a/src/Command/GenerateIriTemplatesCommand.php b/src/Command/GenerateIriTemplatesCommand.php new file mode 100644 index 0000000..3fbcc71 --- /dev/null +++ b/src/Command/GenerateIriTemplatesCommand.php @@ -0,0 +1,92 @@ +addArgument( + 'output', + InputArgument::REQUIRED, + 'The output JSON file path', + ) + ->setDescription('Generate IRI templates and write them to a JSON file') + ->setHelp( + <<<'HELP' + The %command.name% command generates IRI templates from all API Platform resources + and writes them to the specified JSON file. + + php %command.full_name% output.json + php %command.full_name% /path/to/iri-templates.json + HELP + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $outputPath = $input->getArgument('output'); + + try { + $iriTemplates = $this->iriTemplatesService->getIriTemplatesData(); + } catch (Exception $e) { + $io->error(sprintf('Failed to generate IRI templates: %s', $e->getMessage())); + + return Command::FAILURE; + } + + $content = false; + + try { + $content = json_encode($iriTemplates, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } catch (JsonException) { + } + + if ($content === false) { + $io->error('Failed to encode IRI templates to JSON'); + + return Command::FAILURE; + } + + try { + $this->filesystem->dumpFile($outputPath, $content); + $io->success(sprintf('IRI templates written to %s', $outputPath)); + $io->info(sprintf('Generated %d IRI templates', count($iriTemplates))); + + return Command::SUCCESS; + } catch (Exception $e) { + $io->error(sprintf('Failed to write file: %s', $e->getMessage())); + + return Command::FAILURE; + } + } +} diff --git a/src/DependencyInjection/CompilerPass/IriTemplateGeneratorCompilerPass.php b/src/DependencyInjection/CompilerPass/IriTemplateGeneratorCompilerPass.php new file mode 100644 index 0000000..4a3307c --- /dev/null +++ b/src/DependencyInjection/CompilerPass/IriTemplateGeneratorCompilerPass.php @@ -0,0 +1,51 @@ +hasParameter(self::FEATURE_ENABLED_PARAMETER) + || $container->getParameter(self::FEATURE_ENABLED_PARAMETER) === false + ) { + return; + } + + $container + ->setDefinition( + IriTemplatesService::class, + new Definition(IriTemplatesService::class), + ) + ->setArguments([ + new Reference('api_platform.metadata.resource.metadata_collection_factory.cached'), + new Reference('api_platform.metadata.resource.name_collection_factory.cached'), + new Reference('router'), + ]); + + $container + ->setDefinition( + GenerateIriTemplatesCommand::class, + new Definition(GenerateIriTemplatesCommand::class), + ) + ->addTag('console.command') + ->setArguments( + [ + new Reference(IriTemplatesService::class), + new Reference('filesystem'), + ], + ); + } +} diff --git a/src/DependencyInjection/CompilerPass/SchemaProcessorCompilerPass.php b/src/DependencyInjection/CompilerPass/SchemaProcessorCompilerPass.php new file mode 100644 index 0000000..8ecb456 --- /dev/null +++ b/src/DependencyInjection/CompilerPass/SchemaProcessorCompilerPass.php @@ -0,0 +1,38 @@ +hasParameter(self::FEATURE_ENABLED_PARAMETER) + || $container->getParameter(self::FEATURE_ENABLED_PARAMETER) === false + ) { + return; + } + + $container + ->setDefinition( + OpenApiFactory::class, + new Definition(OpenApiFactory::class), + ) + ->setArguments([ + new Reference('api_platform.openapi.factory.inner'), + new TaggedIteratorArgument('netgen_api_platform_extras.open_api_processor'), + ]) + ->setDecoratedService('api_platform.openapi.factory', 'api_platform.openapi.factory.inner', -25); + } +} diff --git a/src/NetgenApiPlatformExtrasBundle.php b/src/NetgenApiPlatformExtrasBundle.php index 3d21b34..1c74c97 100644 --- a/src/NetgenApiPlatformExtrasBundle.php +++ b/src/NetgenApiPlatformExtrasBundle.php @@ -4,6 +4,26 @@ namespace Netgen\ApiPlatformExtras; +use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\IriTemplateGeneratorCompilerPass; +use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\SchemaProcessorCompilerPass; +use Netgen\ApiPlatformExtras\OpenApi\Processor\OpenApiProcessorInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -final class NetgenApiPlatformExtrasBundle extends Bundle {} +final class NetgenApiPlatformExtrasBundle extends Bundle +{ + public function build(ContainerBuilder $container): void + { + $container + ->addCompilerPass( + new IriTemplateGeneratorCompilerPass(), + ) + ->addCompilerPass( + new SchemaProcessorCompilerPass(), + ); + + $container->registerForAutoconfiguration(OpenApiProcessorInterface::class) + ->addTag('netgen_api_platform_extras.open_api_processor') + ->setLazy(true); + } +} diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php new file mode 100644 index 0000000..4583788 --- /dev/null +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -0,0 +1,57 @@ + $processors + */ + public function __construct( + private OpenApiFactoryInterface $decorated, + private iterable $processors, + ) { + $this->processors = $this->sortProcessors($processors); + } + + public function __invoke(array $context = []): OpenApi + { + $openApi = ($this->decorated)($context); + + return $this->applyProcessors($openApi); + } + + private function applyProcessors(OpenApi $openApi): OpenApi + { + foreach ($this->processors as $processor) { + $openApi = $processor->process($openApi); + } + + return $openApi; + } + + /** + * @param iterable<\Netgen\ApiPlatformExtras\OpenApi\Processor\OpenApiProcessorInterface> $processors + * + * @return \Netgen\ApiPlatformExtras\OpenApi\Processor\OpenApiProcessorInterface[] + */ + private function sortProcessors(iterable $processors): array + { + $processors = iterator_to_array($processors); + + usort( + $processors, + static fn ($a, $b): int => $b::getPriority() <=> $a::getPriority(), + ); + + return $processors; + } +} diff --git a/src/OpenApi/Processor/OpenApiProcessorInterface.php b/src/OpenApi/Processor/OpenApiProcessorInterface.php new file mode 100644 index 0000000..5849bfd --- /dev/null +++ b/src/OpenApi/Processor/OpenApiProcessorInterface.php @@ -0,0 +1,21 @@ + + */ + public function getIriTemplatesData(): array + { + $resourceClasses = $this->resourceExtractor->create(); + $routeCollection = $this->router->getRouteCollection(); + $iriTemplates = []; + + foreach ($resourceClasses as $class) { + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($class); + } catch (ResourceClassNotFoundException) { + continue; + } + + /** @var \ApiPlatform\Metadata\ApiResource $resourceMetadata */ + foreach ($resourceMetadataCollection as $resourceMetadata) { + /** @var Operations $operations */ + $operations = $resourceMetadata->getOperations(); + + foreach ($operations as $operation) { + if (!$operation instanceof Get) { + continue; + } + + /** @var string $operationName */ + $operationName = $operation->getName(); + $route = $routeCollection->get($operationName); + + if (!$route instanceof Route) { + continue; + } + + $iriTemplates[$resourceMetadata->getShortName()] = $this->sanitizePath($route->getPath()); + + break; + } + } + } + + return $iriTemplates; + } + + private function sanitizePath(string $path): string + { + return preg_replace( + '/\.\{_format}$/', + '', + $path, + ) ?? ''; + } +}