Skip to content

Commit

Permalink
add operation id builder service
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskapp committed Aug 22, 2024
1 parent 443d39e commit 75e321c
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/ApiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand Down
31 changes: 5 additions & 26 deletions src/Parser/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}
15 changes: 1 addition & 14 deletions src/Parser/Attribute/Meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down
103 changes: 103 additions & 0 deletions src/Parser/Attribute/OperationIdBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <[email protected]>
*
* 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 <[email protected]>
* @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));
}
}
49 changes: 49 additions & 0 deletions src/Parser/Attribute/OperationIdBuilderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/*
* PSX is an open source PHP framework to develop RESTful APIs.
* For the current version and information visit <https://phpsx.org>
*
* Copyright (c) Christoph Kappestein <[email protected]>
*
* 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 <[email protected]>
* @license http://www.apache.org/licenses/LICENSE-2.0
* @link https://phpsx.org
*/
interface OperationIdBuilderInterface
{
public function build(string $controllerClass, string $methodName): string;
}
4 changes: 3 additions & 1 deletion tests/ApiManagerTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
}
4 changes: 3 additions & 1 deletion tests/Console/GenerateCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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));
Expand Down
6 changes: 4 additions & 2 deletions tests/Console/ParseCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions tests/Parser/AttributeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');

Expand All @@ -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');
}

Expand Down
Loading

0 comments on commit 75e321c

Please sign in to comment.