Skip to content

Root-level OA attributes are absorbed into the operations #545

Description

@alexmerlin

Symptom

The generated document has no root servers block, despite #[OA\Server] being declared. externalDocs is duplicated into every operation in the file instead of appearing once at the root.

Observed in the generated YAML before the fix:

  /error-report:
    post:
      ...
      servers:
        -
          url: 'http://api.dotkernel.localhost'
          description: 'Local development server'

grep '^servers:' public/openapi.yaml returned nothing; grep -c externalDocs returned 6 (one per operation) rather than 1.

Evidence

src/App/src/OpenAPI.php declares the document root as floating attributes:

#[OA\Info(version: '1.0', title: 'Dotkernel API')]                                    // line 12
#[OA\Server(url: 'http://api.dotkernel.localhost', description: 'Local development server')]  // line 13
#[OA\SecurityScheme(securityScheme: 'AuthToken', ...)]                                // line 14
#[OA\SecurityScheme(securityScheme: 'ErrorReportingToken', ...)]                      // line 15
#[OA\ExternalDocumentation(...)]                                                      // line 17

/**
 * @see GetIndexResourceHandler::handle()
 */
#[OA\Get(path: '/', ...)]

/**
 * @see PostErrorReportResourceHandler::handle()
 */
#[OA\Post(path: '/error-report', ...)]

// ... schemas ...

class OpenAPI                                                                          // line 196
{
}

Root cause

Every attribute in the file attaches to the single class OpenAPI on line 196 — including the two operations. swagger-php then places each annotation by its declared allowed parents:

  • OpenApi\Annotations\Info::$_parents is [OpenApi::class] only, so Info has nowhere to go but the root. It works.
  • OpenApi\Annotations\Server::$_parents (Server.php:52) is [OpenApi::class, PathItem::class, Operation::class, Get::class, Post::class, Put::class, Delete::class, Patch::class, Head::class, Options::class, Trace::class].
  • ExternalDocumentation::$_parents (ExternalDocumentation.php:50) likewise includes the operation classes.

Because Server and ExternalDocumentation accept an operation as a parent, and operations are present in the same context, they nest into the operations rather than the document root. SecurityScheme survives because its only parents are OpenApi and Components.

This is not a swagger-php bug — the annotations are genuinely valid in both positions, and the attribute context is ambiguous. The declaration has to disambiguate.

Proposed fix

Nest the root metadata inside a single #[OA\OpenApi] attribute. OpenApi\Attributes\OpenApi is #[\Attribute(\Attribute::TARGET_CLASS)] and accepts info, servers, externalDocs and components, which leaves swagger-php no choice about placement:

#[OA\OpenApi(
    info: new OA\Info(version: '1.0', title: 'Dotkernel API'),
    servers: [
        new OA\Server(url: 'http://api.dotkernel.localhost', description: 'Local development server'),
    ],
    externalDocs: new OA\ExternalDocumentation(
        description: 'Dotkernel API documentation',
        url: 'https://docs.dotkernel.org/api-documentation/',
    ),
    components: new OA\Components(
        securitySchemes: [
            new OA\SecurityScheme(
                securityScheme: 'AuthToken',
                type: 'http',
                in: 'header',
                bearerFormat: 'JWT',
                scheme: 'bearer',
            ),
            new OA\SecurityScheme(
                securityScheme: 'ErrorReportingToken',
                type: 'apiKey',
                name: 'Error-Reporting-Token',
                in: 'header',
            ),
        ],
    ),
)]

The two SecuritySchemes move inside OA\Components because they are no longer free-floating; they render in the same place as before.

An equally valid alternative is to move the five root attributes onto a dedicated class carrying no operations — then the root is again the only allowed parent. That keeps the attribute list flat at the cost of a second file. Both were tried in the fork and produce byte-identical output; the single-attribute version was kept to avoid the extra file.

Verification

After the fix the document gains its root block, and the duplicated externalDocs collapses from six copies to one:

openapi: 3.0.0
info:
  title: 'Dotkernel API'
  version: '1.0'
servers:
  -
    url: 'http://api.dotkernel.localhost'
    description: 'Local development server'

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions