Skip to content

Commit

Permalink
Move doctrine annotation loader into Generator
Browse files Browse the repository at this point in the history
Registers a new loader for each `Generator` instance. A new method
`withContext()` was also added to allow to run custom code in the context
of a configured `Generator`.
  • Loading branch information
DerManoMann committed Sep 27, 2021
1 parent 9b3b713 commit 9f3511b
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 80 deletions.
28 changes: 0 additions & 28 deletions src/Analysers/DocBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,10 @@

namespace OpenApi\Analysers;

use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\DocParser;
use OpenApi\Context;
use OpenApi\Generator;

if (class_exists(AnnotationRegistry::class, true)) {
AnnotationRegistry::registerLoader(
function (string $class): bool {
if (DocBlockParser::$whitelist === false) {
$whitelist = ['OpenApi\\Annotations\\'];
} else {
$whitelist = DocBlockParser::$whitelist;
}
foreach ($whitelist as $namespace) {
if (strtolower(substr($class, 0, strlen($namespace))) === strtolower($namespace)) {
$loaded = class_exists($class);
if (!$loaded && $namespace === 'OpenApi\\Annotations\\') {
if (in_array(strtolower(substr($class, 20)), ['definition', 'path'])) {
// Detected an 2.x annotation?
throw new \Exception('The annotation @SWG\\' . substr($class, 20) . '() is deprecated. Found in ' . Generator::$context . "\nFor more information read the migration guide: https://github.com/zircote/swagger-php/blob/master/docs/Migrating-to-v3.md");
}
}

return $loaded;
}
}

return false;
}
);
}

/**
* Extract swagger-php annotations from a [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) using Doctrine's DocParser.
*/
Expand Down
50 changes: 50 additions & 0 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace OpenApi;

use Doctrine\Common\Annotations\AnnotationRegistry;
use OpenApi\Analysers\AnalyserInterface;
use OpenApi\Analysers\AttributeAnnotationFactory;
use OpenApi\Analysers\DocBlockAnnotationFactory;
Expand Down Expand Up @@ -80,17 +81,48 @@ public function __construct(?LoggerInterface $logger = null)

public function push(Generator $generator): void
{
$this->generator = $generator;

// save current state
$this->defaultImports = DocBlockParser::$defaultImports;
$this->whitelist = DocBlockParser::$whitelist;

// update state with generator config
DocBlockParser::$defaultImports = $generator->getAliases();
DocBlockParser::$whitelist = $generator->getNamespaces();

if (class_exists(AnnotationRegistry::class, true)) {
// keeping track of &this->generator allows to 'disable' the loader after we are done;
// no unload, unfortunately :/
$gref = &$this->generator;
AnnotationRegistry::registerLoader(
function (string $class) use (&$gref): bool {
if ($gref) {
foreach ($gref->getNamespaces() as $namespace) {
if (strtolower(substr($class, 0, strlen($namespace))) === strtolower($namespace)) {
$loaded = class_exists($class);
if (!$loaded && $namespace === 'OpenApi\\Annotations\\') {
if (in_array(strtolower(substr($class, 20)), ['definition', 'path'])) {
// Detected an 2.x annotation?
throw new \Exception('The annotation @SWG\\' . substr($class, 20) . '() is deprecated. Found in ' . Generator::$context . "\nFor more information read the migration guide: https://github.com/zircote/swagger-php/blob/master/docs/Migrating-to-v3.md");
}
}

return $loaded;
}
}
}

return false;
}
);
}
}

public function pop(): void
{
$this->generator = null;

DocBlockParser::$defaultImports = $this->defaultImports;
DocBlockParser::$whitelist = $this->whitelist;
}
Expand Down Expand Up @@ -279,6 +311,24 @@ public static function scan(iterable $sources, array $options = []): ?OpenApi
->generate($sources, $config['analysis'], $config['validate']);
}

/**
* Run code in the context of this generator.
*
* @return mixed the result of the `callable`
*/
public function withContext(callable $callable)
{
$rootContext = new Context(['logger' => $this->getLogger()]);
$analysis = new Analysis([], $rootContext);

$this->configStack->push($this);
try {
return $callable($this, $analysis, $rootContext);
} finally {
$this->configStack->pop();
}
}

/**
* Generate OpenAPI spec by scanning the given source files.
*
Expand Down
4 changes: 2 additions & 2 deletions tests/Analysers/DocBlockParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected function tearDown(): void

public function testParseContents()
{
$annotations = $this->annotationsFromDocBlock('@OA\Parameter(description="This is my parameter")');
$annotations = $this->annotationsFromDocBlockParser('@OA\Parameter(description="This is my parameter")');
$this->assertIsArray($annotations);
$parameter = $annotations[0];
$this->assertInstanceOf('OpenApi\Annotations\Parameter', $parameter);
Expand All @@ -35,6 +35,6 @@ public function testParseContents()
public function testDeprecatedAnnotationWarning()
{
$this->assertOpenApiLogEntryContains('The annotation @SWG\Definition() is deprecated.');
$this->annotationsFromDocBlock('@SWG\Definition()');
$this->annotationsFromDocBlockParser('@SWG\Definition()');
}
}
25 changes: 13 additions & 12 deletions tests/Analysers/ReflectionAnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ public function analysers()
*/
public function testApiDocBlockBasic(AnalyserInterface $analyser)
{
$fixture = $this->fixture('Apis/DocBlocks/basic.php');
//require_once $fixture;
$analysis = (new Generator())
->withContext(function (Generator $generator) use ($analyser) {
$analysis = $analyser->fromFile($this->fixture('Apis/DocBlocks/basic.php'), $this->getContext());
$analysis->process($generator->getProcessors());

$analysis = $analyser->fromFile($fixture, $this->getContext());
$analysis->process((new Generator())->getProcessors());
return $analysis;
});

$operations = $analysis->getAnnotationsOfType(Operation::class);
$this->assertIsArray($operations);
Expand All @@ -100,10 +102,7 @@ public function testApiDocBlockBasic(AnalyserInterface $analyser)
*/
public function testApiAttributesBasic(AnalyserInterface $analyser)
{
$fixture = $this->fixture('Apis/Attributes/basic.php');
require_once $fixture;

$analysis = $analyser->fromFile($fixture, $this->getContext());
$analysis = $analyser->fromFile($this->fixture('Apis/Attributes/basic.php'), $this->getContext());
$analysis->process((new Generator())->getProcessors());

$operations = $analysis->getAnnotationsOfType(Operation::class);
Expand All @@ -121,11 +120,13 @@ public function testApiAttributesBasic(AnalyserInterface $analyser)
*/
public function testApiMixedBasic(AnalyserInterface $analyser)
{
$fixture = $this->fixture('Apis/Mixed/basic.php');
require_once $fixture;
$analysis = (new Generator())
->withContext(function (Generator $generator) use ($analyser) {
$analysis = $analyser->fromFile($this->fixture('Apis/Mixed/basic.php'), $this->getContext());
$analysis->process((new Generator())->getProcessors());

$analysis = $analyser->fromFile($fixture, $this->getContext());
$analysis->process((new Generator())->getProcessors());
return $analysis;
});

$operations = $analysis->getAnnotationsOfType(Operation::class);
$this->assertIsArray($operations);
Expand Down
16 changes: 8 additions & 8 deletions tests/Annotations/AbstractAnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class AbstractAnnotationTest extends OpenApiTestCase
{
public function testVendorFields()
{
$annotations = $this->annotationsFromDocBlock('@OA\Get(x={"internal-id": 123})');
$annotations = $this->annotationsFromDocBlockParser('@OA\Get(x={"internal-id": 123})');
$output = $annotations[0]->jsonSerialize();
$prefixedProperty = 'x-internal-id';
$this->assertSame(123, $output->$prefixedProperty);
Expand All @@ -24,13 +24,13 @@ public function testVendorFields()
public function testInvalidField()
{
$this->assertOpenApiLogEntryContains('Unexpected field "doesnot" for @OA\Get(), expecting');
$this->annotationsFromDocBlock('@OA\Get(doesnot="exist")');
$this->annotationsFromDocBlockParser('@OA\Get(doesnot="exist")');
}

public function testUmergedAnnotation()
{
$openapi = $this->createOpenApiWithInfo();
$openapi->merge($this->annotationsFromDocBlock('@OA\Items()'));
$openapi->merge($this->annotationsFromDocBlockParser('@OA\Items()'));
$this->assertOpenApiLogEntryContains('Unexpected @OA\Items(), expected to be inside @OA\\');
$openapi->validate();
}
Expand All @@ -45,7 +45,7 @@ public function testConflictedNesting()
@OA\Contact(name="second")
)
END;
$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$this->assertOpenApiLogEntryContains('Only one @OA\Contact() allowed for @OA\Info() multiple found in:');
$annotations[0]->validate();
}
Expand All @@ -57,7 +57,7 @@ public function testKey()
@OA\Header(header="X-CSRF-Token",description="Token to prevent Cross Site Request Forgery")
)
END;
$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$this->assertEquals('{"headers":{"X-CSRF-Token":{"description":"Token to prevent Cross Site Request Forgery"}}}', json_encode($annotations[0]));
}

Expand All @@ -70,14 +70,14 @@ public function testConflictingKey()
@OA\Header(header="X-CSRF-Token", @OA\Schema(type="string"), description="second")
)
END;
$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$this->assertOpenApiLogEntryContains('Multiple @OA\Header() with the same header="X-CSRF-Token":');
$annotations[0]->validate();
}

public function testRequiredFields()
{
$annotations = $this->annotationsFromDocBlock('@OA\Info()');
$annotations = $this->annotationsFromDocBlockParser('@OA\Info()');
$info = $annotations[0];
$this->assertOpenApiLogEntryContains('Missing required field "title" for @OA\Info() in ');
$this->assertOpenApiLogEntryContains('Missing required field "version" for @OA\Info() in ');
Expand All @@ -96,7 +96,7 @@ public function testTypeValidation()
)
)
END;
$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$parameter = $annotations[0];
$this->assertOpenApiLogEntryContains('@OA\Parameter(name=123,in="dunno")->name is a "integer", expecting a "string" in ');
$this->assertOpenApiLogEntryContains('@OA\Parameter(name=123,in="dunno")->in "dunno" is invalid, expecting "query", "header", "path", "cookie" in ');
Expand Down
6 changes: 3 additions & 3 deletions tests/Annotations/ItemsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ class ItemsTest extends OpenApiTestCase
{
public function testItemTypeArray()
{
$annotations = $this->annotationsFromDocBlock('@OA\Items(type="array")');
$annotations = $this->annotationsFromDocBlockParser('@OA\Items(type="array")');
$this->assertOpenApiLogEntryContains('@OA\Items() is required when @OA\Items() has type "array" in ');
$annotations[0]->validate();
}

public function testSchemaTypeArray()
{
$annotations = $this->annotationsFromDocBlock('@OA\Schema(type="array")');
$annotations = $this->annotationsFromDocBlockParser('@OA\Schema(type="array")');
$this->assertOpenApiLogEntryContains('@OA\Items() is required when @OA\Schema() has type "array" in ');
$annotations[0]->validate();
}

public function testParentTypeArray()
{
$annotations = $this->annotationsFromDocBlock('@OA\Items() parent type must be "array"');
$annotations = $this->annotationsFromDocBlockParser('@OA\Items() parent type must be "array"');
$annotations[0]->validate();
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Annotations/OperationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function testSecuritySerialization($security, $dockBlock, $expected)
$json = $operation->toJson($flags);
$this->assertEquals($expected, $json);

$annotations = $this->annotationsFromDocBlock($dockBlock);
$annotations = $this->annotationsFromDocBlockParser($dockBlock);
$this->assertCount(1, $annotations);
$json = $annotations[0]->toJson($flags);
$this->assertEquals($expected, $json);
Expand Down
2 changes: 1 addition & 1 deletion tests/Annotations/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function testWrongRangeDefinition()

protected function validateMisspelledAnnotation(string $response = '')
{
$annotations = $this->annotationsFromDocBlock(
$annotations = $this->annotationsFromDocBlockParser(
'@OA\Get(@OA\Response(response="' . $response . '", description="description"))'
);
/*
Expand Down
6 changes: 3 additions & 3 deletions tests/Annotations/SecuritySchemesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function testParseServers()
*/
INFO;
$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);

$this->assertCount(3, $annotations);
$this->assertInstanceOf(Info::class, $annotations[0]);
Expand Down Expand Up @@ -79,7 +79,7 @@ public function testImplicitFlowAnnotation()
*/
SCHEME;

$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$this->assertCount(1, $annotations);
/** @var \OpenApi\Annotations\SecurityScheme $security */
$security = $annotations[0];
Expand Down Expand Up @@ -119,7 +119,7 @@ public function testMultipleAnnotations()
*/
SCHEME;

$annotations = $this->annotationsFromDocBlock($comment);
$annotations = $this->annotationsFromDocBlockParser($comment);
$this->assertCount(1, $annotations);
/** @var \OpenApi\Annotations\SecurityScheme $security */
$security = $annotations[0];
Expand Down
14 changes: 7 additions & 7 deletions tests/ConstantsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,34 @@ public function testConstant()
$const = 'OPENAPI_TEST_' . self::$counter;
$this->assertFalse(defined($const));
$this->assertOpenApiLogEntryContains("[Semantical Error] Couldn't find constant " . $const);
$this->annotationsFromDocBlock('@OA\Contact(email=' . $const . ')');
$this->annotationsFromDocBlockParser('@OA\Contact(email=' . $const . ')');

define($const, 'me@domain.org');
$annotations = $this->annotationsFromDocBlock('@OA\Contact(email=' . $const . ')');
$annotations = $this->annotationsFromDocBlockParser('@OA\Contact(email=' . $const . ')');
$this->assertSame('me@domain.org', $annotations[0]->email);
}

public function testFQCNConstant()
{
$annotations = $this->annotationsFromDocBlock('@OA\Contact(url=OpenApi\Tests\ConstantsTest::URL)');
$annotations = $this->annotationsFromDocBlockParser('@OA\Contact(url=OpenApi\Tests\ConstantsTest::URL)');
$this->assertSame('http://example.com', $annotations[0]->url);

$annotations = $this->annotationsFromDocBlock('@OA\Contact(url=\OpenApi\Tests\ConstantsTest::URL)');
$annotations = $this->annotationsFromDocBlockParser('@OA\Contact(url=\OpenApi\Tests\ConstantsTest::URL)');
$this->assertSame('http://example.com', $annotations[0]->url);
}

public function testInvalidClass()
{
$this->assertOpenApiLogEntryContains("[Semantical Error] Couldn't find constant ConstantsTest::URL");
$this->annotationsFromDocBlock('@OA\Contact(url=ConstantsTest::URL)');
$this->annotationsFromDocBlockParser('@OA\Contact(url=ConstantsTest::URL)');
}

public function testAutoloadConstant()
{
if (class_exists('AnotherNamespace\Annotations\Constants', false)) {
$this->markTestSkipped();
}
$annotations = $this->annotationsFromDocBlock('@OA\Contact(name=AnotherNamespace\Annotations\Constants::INVALID_TIMEZONE_LOCATION)');
$annotations = $this->annotationsFromDocBlockParser('@OA\Contact(name=AnotherNamespace\Annotations\Constants::INVALID_TIMEZONE_LOCATION)');
$this->assertSame('invalidTimezoneLocation', $annotations[0]->name);
}

Expand All @@ -73,7 +73,7 @@ public function testDefaultImports()
'contact' => 'OpenApi\Annotations\Contact', // use OpenApi\Annotations\Contact;
'ctest' => 'OpenApi\Tests\ConstantsTesT', // use OpenApi\Tests\ConstantsTesT as CTest;
];
$annotations = $this->annotationsFromDocBlock('@Contact(url=CTest::URL)');
$annotations = $this->annotationsFromDocBlockParser('@Contact(url=CTest::URL)');
$this->assertSame('http://example.com', $annotations[0]->url);
DocBlockParser::$defaultImports = $backup;
}
Expand Down
9 changes: 6 additions & 3 deletions tests/OpenApiTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,15 @@ public function analysisFromCode(string $code, ?Context $context = null): Analys
return $analyser->fromFile($tmpFile, $context ?: $this->getContext());
}

protected function annotationsFromDocBlock($docBlock, ?Context $context = null): array
protected function annotationsFromDocBlockParser($docBlock, ?Context $context = null): array
{
$docBlockParser = new DocBlockParser();
$context = $context ?: $this->getContext();

return $docBlockParser->fromComment($docBlock, $context);
return (new Generator())->withContext(function () use ($context, $docBlock) {
$docBlockParser = new DocBlockParser();

return $docBlockParser->fromComment($docBlock, $context);
});
}

/**
Expand Down
Loading

0 comments on commit 9f3511b

Please sign in to comment.