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,
+ ) ?? '';
+ }
+}