Skip to content
92 changes: 92 additions & 0 deletions src/Command/GenerateIriTemplatesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\Command;

use Exception;
use JsonException;
use Netgen\ApiPlatformExtras\Service\IriTemplatesService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;

use function count;
use function json_encode;
use function sprintf;

use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;

final class GenerateIriTemplatesCommand extends Command
{
public function __construct(
private IriTemplatesService $iriTemplatesService,
private Filesystem $filesystem,
) {
parent::__construct('netgen:api-platform-extras:generate-iri-templates');
}

protected function configure(): void
{
$this
->addArgument(
'output',
InputArgument::REQUIRED,
'The output JSON file path',
)
->setDescription('Generate IRI templates and write them to a JSON file')
->setHelp(
<<<'HELP'
The <info>%command.name%</info> command generates IRI templates from all API Platform resources
and writes them to the specified JSON file.

<info>php %command.full_name% output.json</info>
<info>php %command.full_name% /path/to/iri-templates.json</info>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass;

use Netgen\ApiPlatformExtras\Command\GenerateIriTemplatesCommand;
use Netgen\ApiPlatformExtras\Service\IriTemplatesService;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

final class IriTemplateGeneratorCompilerPass implements CompilerPassInterface
{
private const string FEATURE_ENABLED_PARAMETER = 'netgen_api_platform_extras.features.iri_template_generator.enabled';

public function process(ContainerBuilder $container): void
{
if (
!$container->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'),
],
);
}
}
12 changes: 11 additions & 1 deletion src/NetgenApiPlatformExtrasBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

namespace Netgen\ApiPlatformExtras;

use Netgen\ApiPlatformExtras\DependencyInjection\CompilerPass\IriTemplateGeneratorCompilerPass;
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(),
);
}
}
78 changes: 78 additions & 0 deletions src/Service/IriTemplatesService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Netgen\ApiPlatformExtras\Service;

use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;

use function preg_replace;

final class IriTemplatesService
{
public function __construct(
private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
private ResourceNameCollectionFactoryInterface $resourceExtractor,
private RouterInterface $router,
) {}

/**
* @return array<string, string>
*/
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<HttpOperation> $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,
) ?? '';
}
}