From 75e321ce585f1095485206bdbcaa59a3bef53e6f Mon Sep 17 00:00:00 2001 From: Christoph Kappestein Date: Thu, 22 Aug 2024 18:22:46 +0200 Subject: [PATCH] add operation id builder service --- src/ApiManager.php | 5 +- src/Parser/Attribute.php | 31 +----- src/Parser/Attribute/Meta.php | 15 +-- src/Parser/Attribute/OperationIdBuilder.php | 103 ++++++++++++++++++ .../Attribute/OperationIdBuilderInterface.php | 49 +++++++++ tests/ApiManagerTestCase.php | 4 +- tests/Console/GenerateCommandTest.php | 4 +- tests/Console/ParseCommandTest.php | 6 +- tests/Parser/AttributeTest.php | 6 +- tests/Transformer/OpenAPITest.php | 4 +- 10 files changed, 178 insertions(+), 49 deletions(-) create mode 100644 src/Parser/Attribute/OperationIdBuilder.php create mode 100644 src/Parser/Attribute/OperationIdBuilderInterface.php diff --git a/src/ApiManager.php b/src/ApiManager.php index b46719c..84c9af1 100644 --- a/src/ApiManager.php +++ b/src/ApiManager.php @@ -24,6 +24,7 @@ use PSX\Api\Builder\SpecificationBuilder; use PSX\Api\Builder\SpecificationBuilderInterface; use PSX\Api\Exception\InvalidApiException; +use PSX\Api\Parser\Attribute\OperationIdBuilderInterface; use PSX\Schema\Parser\ContextInterface; use PSX\Schema\SchemaManagerInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -42,12 +43,12 @@ class ApiManager implements ApiManagerInterface private array $parsers = []; - public function __construct(SchemaManagerInterface $schemaManager, ?CacheItemPoolInterface $cache = null, bool $debug = false) + public function __construct(SchemaManagerInterface $schemaManager, OperationIdBuilderInterface $operationIdBuilder, ?CacheItemPoolInterface $cache = null, bool $debug = false) { $this->cache = $cache === null ? new ArrayAdapter() : $cache; $this->debug = $debug; - $this->register('php', new Parser\Attribute($schemaManager)); + $this->register('php', new Parser\Attribute($schemaManager, $operationIdBuilder)); $this->register('file', new Parser\File($schemaManager)); $this->register('openapi', new Parser\OpenAPI($schemaManager)); $this->register('typeapi', new Parser\TypeAPI($schemaManager)); diff --git a/src/Parser/Attribute.php b/src/Parser/Attribute.php index 7b7472e..5368612 100644 --- a/src/Parser/Attribute.php +++ b/src/Parser/Attribute.php @@ -27,6 +27,7 @@ use PSX\Api\Model\Passthru; use PSX\Api\Operation; use PSX\Api\Parser\Attribute\Meta; +use PSX\Api\Parser\Attribute\OperationIdBuilderInterface; use PSX\Api\ParserInterface; use PSX\Api\Specification; use PSX\Api\SpecificationInterface; @@ -62,11 +63,13 @@ class Attribute implements ParserInterface { private SchemaManagerInterface $schemaManager; + private OperationIdBuilderInterface $operationIdBuilder; private bool $inspectTypeHints; - public function __construct(SchemaManagerInterface $schemaManager, bool $inspectTypeHints = true) + public function __construct(SchemaManagerInterface $schemaManager, OperationIdBuilderInterface $operationIdBuilder, bool $inspectTypeHints = true) { $this->schemaManager = $schemaManager; + $this->operationIdBuilder = $operationIdBuilder; $this->inspectTypeHints = $inspectTypeHints; } @@ -112,10 +115,7 @@ private function parseMethods(ReflectionClass $controller, SpecificationInterfac continue; } - $operationId = $meta->getOperationId()?->operationId; - if (empty($operationId)) { - $operationId = self::buildOperationId($controller->getName(), $method->getName()); - } + $operationId = $this->operationIdBuilder->build($controller->getName(), $method->getName()); if ($specification->getOperations()->has($operationId)) { continue; @@ -570,25 +570,4 @@ private function getTypeFromEnum(\ReflectionEnum $enum, string $name, bool $requ return $this->getParamArgsFromType($name, $enum->getBackingType(), $required, $values); } - - public static function buildOperationId(string $controllerName, string $methodName): string - { - $result = []; - $parts = explode('\\', $controllerName); - array_shift($parts); // vendor - array_shift($parts); // controller - - foreach ($parts as $part) { - $result[] = self::snakeCase($part); - } - - $result[] = $methodName; - - return implode('.', $result); - } - - private static function snakeCase(string $name): string - { - return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); - } } diff --git a/src/Parser/Attribute/Meta.php b/src/Parser/Attribute/Meta.php index 067994c..dd84812 100644 --- a/src/Parser/Attribute/Meta.php +++ b/src/Parser/Attribute/Meta.php @@ -27,13 +27,12 @@ use PSX\Api\Attribute\HeaderParam; use PSX\Api\Attribute\Incoming; use PSX\Api\Attribute\MethodAbstract; -use PSX\Api\Attribute\OperationId; use PSX\Api\Attribute\Outgoing; use PSX\Api\Attribute\Path; use PSX\Api\Attribute\PathParam; use PSX\Api\Attribute\QueryParam; -use PSX\Api\Attribute\StatusCode; use PSX\Api\Attribute\Security; +use PSX\Api\Attribute\StatusCode; use PSX\Api\Attribute\Tags; /** @@ -66,7 +65,6 @@ class Meta * @var Outgoing[] */ private array $outgoing = []; - private ?OperationId $operationId = null; private ?Tags $tags = null; private ?Security $security = null; private ?Deprecated $deprecated = null; @@ -94,8 +92,6 @@ public function __construct(array $attributes) $this->incoming = $attribute; } elseif ($attribute instanceof Outgoing) { $this->outgoing[$attribute->code] = $attribute; - } elseif ($attribute instanceof OperationId) { - $this->operationId = $attribute; } elseif ($attribute instanceof Tags) { $this->tags = $attribute; } elseif ($attribute instanceof Security) { @@ -138,10 +134,6 @@ public function merge(Meta $meta) $this->outgoing = array_merge($this->outgoing, $meta->getOutgoing()); - if ($this->operationId === null) { - $this->operationId = $meta->getOperationId(); - } - if ($this->tags === null) { $this->tags = $meta->getTags(); } @@ -253,11 +245,6 @@ public function hasOutgoing(): bool return count($this->outgoing) > 0; } - public function getOperationId(): ?OperationId - { - return $this->operationId; - } - public function getTags(): ?Tags { return $this->tags; diff --git a/src/Parser/Attribute/OperationIdBuilder.php b/src/Parser/Attribute/OperationIdBuilder.php new file mode 100644 index 0000000..dd93b43 --- /dev/null +++ b/src/Parser/Attribute/OperationIdBuilder.php @@ -0,0 +1,103 @@ + + * + * Copyright (c) Christoph Kappestein + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PSX\Api\Parser\Attribute; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use PSX\Api\Attribute\OperationId; + +/** + * OperationIdBuilder + * + * @author Christoph Kappestein + * @license http://www.apache.org/licenses/LICENSE-2.0 + * @link https://phpsx.org + */ +class OperationIdBuilder implements OperationIdBuilderInterface +{ + private CacheItemPoolInterface $cache; + private bool $debug; + + public function __construct(CacheItemPoolInterface $cache, bool $debug) + { + $this->cache = $cache; + $this->debug = $debug; + } + + public function build(string $controllerClass, string $methodName): string + { + $item = null; + if (!$this->debug) { + $key = 'psx_operation_id_' . str_replace('\\', '_', $controllerClass) . '_' . $methodName; + $item = $this->cache->getItem($key); + if ($item->isHit()) { + return $item->get(); + } + } + + $operationId = $this->getByOperationIdAttribute($controllerClass, $methodName); + if ($operationId === null) { + $operationId = $this->buildByClassAndMethodName($controllerClass, $methodName); + } + + if (!$this->debug && $item instanceof CacheItemInterface) { + $item->set($operationId); + $this->cache->save($item); + } + + return $operationId; + } + + private function getByOperationIdAttribute(string $controllerClass, string $methodName): ?string + { + $method = new \ReflectionMethod($controllerClass, $methodName); + $attributes = $method->getAttributes(OperationId::class); + foreach ($attributes as $attribute) { + $operation = $attribute->newInstance(); + if ($operation instanceof OperationId) { + return $operation->operationId; + } + } + + return null; + } + + private function buildByClassAndMethodName(string $controllerClass, string $methodName): string + { + $result = []; + $parts = explode('\\', $controllerClass); + array_shift($parts); // vendor + array_shift($parts); // controller + + foreach ($parts as $part) { + $result[] = $this->snakeCase($part); + } + + $result[] = $methodName; + + return implode('.', $result); + } + + private function snakeCase(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + } +} diff --git a/src/Parser/Attribute/OperationIdBuilderInterface.php b/src/Parser/Attribute/OperationIdBuilderInterface.php new file mode 100644 index 0000000..5aa6fd9 --- /dev/null +++ b/src/Parser/Attribute/OperationIdBuilderInterface.php @@ -0,0 +1,49 @@ + + * + * Copyright (c) Christoph Kappestein + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace PSX\Api\Parser\Attribute; + +use PSX\Api\Attribute\Authorization; +use PSX\Api\Attribute\Deprecated; +use PSX\Api\Attribute\Description; +use PSX\Api\Attribute\Exclude; +use PSX\Api\Attribute\HeaderParam; +use PSX\Api\Attribute\Incoming; +use PSX\Api\Attribute\MethodAbstract; +use PSX\Api\Attribute\OperationId; +use PSX\Api\Attribute\Outgoing; +use PSX\Api\Attribute\Path; +use PSX\Api\Attribute\PathParam; +use PSX\Api\Attribute\QueryParam; +use PSX\Api\Attribute\StatusCode; +use PSX\Api\Attribute\Security; +use PSX\Api\Attribute\Tags; + +/** + * OperationIdBuilder + * + * @author Christoph Kappestein + * @license http://www.apache.org/licenses/LICENSE-2.0 + * @link https://phpsx.org + */ +interface OperationIdBuilderInterface +{ + public function build(string $controllerClass, string $methodName): string; +} diff --git a/tests/ApiManagerTestCase.php b/tests/ApiManagerTestCase.php index 546bf52..5644491 100644 --- a/tests/ApiManagerTestCase.php +++ b/tests/ApiManagerTestCase.php @@ -22,7 +22,9 @@ use PHPUnit\Framework\TestCase; use PSX\Api\ApiManager; +use PSX\Api\Parser\Attribute\OperationIdBuilder; use PSX\Schema\SchemaManager; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * ApiManagerTest @@ -39,6 +41,6 @@ abstract class ApiManagerTestCase extends TestCase protected function setUp(): void { $this->schemaManager = new SchemaManager(); - $this->apiManager = new ApiManager($this->schemaManager); + $this->apiManager = new ApiManager($this->schemaManager, new OperationIdBuilder(new ArrayAdapter(), false)); } } diff --git a/tests/Console/GenerateCommandTest.php b/tests/Console/GenerateCommandTest.php index 4a204c4..a654f1f 100644 --- a/tests/Console/GenerateCommandTest.php +++ b/tests/Console/GenerateCommandTest.php @@ -24,10 +24,12 @@ use PSX\Api\ApiManager; use PSX\Api\Console\GenerateCommand; use PSX\Api\GeneratorFactory; +use PSX\Api\Parser\Attribute\OperationIdBuilder; use PSX\Api\Repository\LocalRepository; use PSX\Api\Scanner\Memory; use PSX\Api\Tests\Parser\Attribute\TestController; use PSX\Schema\SchemaManager; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Console\Tester\CommandTester; /** @@ -68,7 +70,7 @@ public function testGenerateSpecOpenAPI() protected function getGenerateCommand() { - $apiManager = new ApiManager(new SchemaManager()); + $apiManager = new ApiManager(new SchemaManager(), new OperationIdBuilder(new ArrayAdapter(), false)); $scanner = new Memory(); $scanner->merge($apiManager->getApi(TestController::class)); diff --git a/tests/Console/ParseCommandTest.php b/tests/Console/ParseCommandTest.php index b60bdbc..2f61e30 100644 --- a/tests/Console/ParseCommandTest.php +++ b/tests/Console/ParseCommandTest.php @@ -24,9 +24,11 @@ use PSX\Api\ApiManager; use PSX\Api\Console\ParseCommand; use PSX\Api\GeneratorFactory; +use PSX\Api\Parser\Attribute\OperationIdBuilder; use PSX\Api\Repository\LocalRepository; use PSX\Api\Tests\Parser\Attribute\TestController; use PSX\Schema\SchemaManager; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Console\Tester\CommandTester; /** @@ -55,9 +57,9 @@ public function testGenerateSpecOpenAPI() $this->assertJsonStringEqualsJsonString($expect, $actual, $actual); } - protected function getParseCommand() + protected function getParseCommand(): ParseCommand { - $apiManager = new ApiManager(new SchemaManager()); + $apiManager = new ApiManager(new SchemaManager(), new OperationIdBuilder(new ArrayAdapter(), false)); $factory = GeneratorFactory::fromLocal('http://foo.com/'); return new ParseCommand($apiManager, $factory); diff --git a/tests/Parser/AttributeTest.php b/tests/Parser/AttributeTest.php index 4ebddac..9cdd7ae 100644 --- a/tests/Parser/AttributeTest.php +++ b/tests/Parser/AttributeTest.php @@ -23,11 +23,13 @@ use PSX\Api\Exception\ParserException; use PSX\Api\OperationInterface; use PSX\Api\Parser\Attribute as AttributeParser; +use PSX\Api\Parser\Attribute\OperationIdBuilder; use PSX\Api\SpecificationInterface; use PSX\Api\Tests\Parser\Attribute\BarController; use PSX\Api\Tests\Parser\Attribute\PropertyController; use PSX\Api\Tests\Parser\Attribute\TestController; use PSX\Schema\TypeFactory; +use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * AttributeTest @@ -57,7 +59,7 @@ public function testOperationId() public function testParseTypeHint() { - $annotation = new AttributeParser($this->schemaManager); + $annotation = new AttributeParser($this->schemaManager, new OperationIdBuilder(new ArrayAdapter(), false)); $specification = $annotation->parse(BarController::class); $operation = $specification->getOperations()->get('tests.parser.attribute.bar_controller.myMethod'); @@ -76,7 +78,7 @@ public function testParseInvalid() { $this->expectException(ParserException::class); - $annotation = new AttributeParser($this->schemaManager); + $annotation = new AttributeParser($this->schemaManager, new OperationIdBuilder(new ArrayAdapter(), false)); $annotation->parse('foo'); } diff --git a/tests/Transformer/OpenAPITest.php b/tests/Transformer/OpenAPITest.php index cf70515..90ac347 100644 --- a/tests/Transformer/OpenAPITest.php +++ b/tests/Transformer/OpenAPITest.php @@ -22,9 +22,11 @@ use PHPUnit\Framework\TestCase; use PSX\Api\ApiManager; +use PSX\Api\Parser\Attribute\OperationIdBuilder; use PSX\Api\SpecificationInterface; use PSX\Api\Transformer\OpenAPI; use PSX\Schema\SchemaManager; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Yaml\Yaml; /** @@ -55,7 +57,7 @@ public function testConvert(string $file) $this->assertJsonStringEqualsJsonFile($expectFile, \json_encode($actual)); // test whether we can parse the spec file - $spec = (new ApiManager(new SchemaManager()))->getApi($expectFile); + $spec = (new ApiManager(new SchemaManager(), new OperationIdBuilder(new ArrayAdapter(), false)))->getApi($expectFile); $this->assertInstanceOf(SpecificationInterface::class, $spec); }