Migrating to spec attributes 🧪 ​
Moving a codebase from OpenApi\Attributes to OpenApi\Spec changes class names, argument names and, in places, the shape of the attributes. The differences from classic attributes list them. swagger-php ships a Rector set that makes those changes.
Annotations in docblocks are out of scope: the set reads PHP attributes only. Rector rules for moving annotations to classic attributes first are collected in #1047.
Before you start ​
Generate your document in classic mode and keep it: the migration is done when spec mode generates the same document from the migrated code.
Spec mode defaults to OpenAPI 3.1.0 and classic to 3.0.0, so pass the version explicitly to both: --version on the command line, Builder::setVersion() in code.
Running the Rector set ​
composer require --dev rector/rector
./vendor/bin/rector process src \
--config vendor/zircote/swagger-php/rector/set/classic-to-spec.php --dry-runDrop --dry-run once the diff looks right.
Point it at every directory with code that uses an attribute, not only the directories your generator scans. That includes classes that extend one, or construct one with new. A class extending OpenApi\Attributes\Response that lives outside the scanned paths is migrated by this run or not at all.
The set:
- names positional arguments by the classic constructor's parameters, including
parent::__construct()in your own subclasses. The spec classes order their parameters differently, so a positional call would bind to the wrong ones after the rename - lifts
info:,servers:,tags:andexternalDocs:off a rootOpenApiattribute into sibling attributes, and renames itsopenapi:toversion: - wraps a bare
enum: Suit::classintoenum: [Suit::class] - turns a class-string
type: Pet::classintoref: Pet::class - rewrites the type names classic maps, such as
type: 'long', to the OpenAPI type and format they stand for,type: 'integer', format: 'int64' - turns each item of
security:into aSecurity\Requirement. A bare scheme name,'bearerAuth', or an array,['bearerAuth' => []]or['bearerAuth'], becomesnew Security\Requirement(scheme: 'bearerAuth'), withscopes:where it has any, and several schemes in one array becomeschemes:. Spec drops a string there, with a warning, and compiles an array only as a deprecated form. Items that are not literals, such as a constant, are left for you - adds
#[Components]to a class whose members declare keyed components, since spec takes a component on a member only in a holder. A class with aSchemaof its own is left alone, and so is a keyed attribute that something beside it nests, such as a response beside an operation - renames the classes,
OpenApi\Attributes\GettoOpenApi\Spec\Operation\Getand so on - moves schema keywords off
Property,JsonContentandXmlContentinto a nestedschema: new Schema(...), since the spec classes no longer accept them. Aschema:written as a reference,'#/components/schemas/Pet'orPet::class, becomes that schema'sref: - imports the spec namespace as
use OpenApi\Spec as OAS;
Formatting is left to your code style tooling.
What it leaves for you ​
A header beside several responses ​
Classic resolves a #[Header] stacked beside more than one #[Response] by copying it into every one of them:
#[OA\Get(path: '/pets/{id}', operationId: 'getPet')]
#[OA\Response(response: 200, description: 'The pet')]
#[OA\Response(response: 404, description: 'No such pet')]
#[OA\Header(header: 'X-Request-Id', description: 'Correlates the request')]
public function get(): voidresponses:
'200':
description: 'The pet'
headers:
X-Request-Id:
description: 'Correlates the request'
'404':
description: 'No such pet'
headers:
X-Request-Id:
description: 'Correlates the request'Spec mode reports Ambiguous merge instead and skips the class. Which response the header belongs to is not something the code says, so the set does not guess. Nest it in the response it belongs to, or give it a component: and reference it from each. See Components.
Responses beside several operations ​
The same happens one level up. A method carrying more than one operation attribute, with #[Response]s stacked beside them, gets every response copied into every operation in classic:
#[OA\Get(path: '/pets/{id}', operationId: 'getPet')]
#[OA\Post(path: '/pets/{id}', operationId: 'postPet')]
#[OA\Response(response: 200, description: 'The pet')]
#[OA\Response(response: 404, description: 'No such pet')]
public function pet(): voidSpec mode reports Ambiguous merge for the first Response and skips the class, which drops both operations from the document. Give each operation its own responses::
#[OA\Operation\Get(path: '/pets/{id}', operationId: 'getPet', responses: [
new OA\Response(response: 200, description: 'The pet'),
new OA\Response(response: 404, description: 'No such pet'),
])]
#[OA\Operation\Post(path: '/pets/{id}', operationId: 'postPet', responses: [
new OA\Response(response: 200, description: 'The pet'),
new OA\Response(response: 404, description: 'No such pet'),
])]
public function pet(): voidA response both operations share can be declared once with a component: and referenced as new OA\Response(response: 404, ref: '#/components/responses/NoSuchPet') from each.
Constructs with no one-to-one replacement ​
The set leaves these as they are:
OpenApi\Attributes\Queryis the HTTPQUERYoperation, which is OpenAPI 3.2. In spec it is#[OA\Operation(path: '/search', method: 'query')].OpenApi\Attributes\Webhookbecomes awebhook:argument on an operation attribute, in place ofpath:.OpenApi\Attributes\PathItemis renamed, but the specPathItemis a different concept and takes different arguments.- A root
OpenApicarryingpaths:,components:orwebhooks:, or children that are not written inline asnewexpressions, is not un-nested. - Attributes on a constructor stay where they are, and spec ignores them with a warning. Move them to the class. See Nothing is read from a constructor.
- A
Property,JsonContentorXmlContentthat already has its ownschema:beside other schema keywords is left whole, and fails at generation time withUnknown named parameter. Move the keywords into the nested schema by hand. The same holds for aschema:string that is neither a pointer nor a::class, and for one with aref:beside it.
The component keys ​
Spec attributes name a reusable component with component:. The classic spellings, schema:, parameter:, request: and securityScheme:, still work on the spec attributes until 8.0, generate the same document, and are left alone by the set. Each use triggers a PHP deprecation, which appears wherever your application reports deprecations.
Generator code ​
The set rewrites attributes. Code that drives the generator is yours to move:
- The mode.
Builder::setMode(Mode::SPEC), or--mode specon the command line. - Custom processors. Spec mode has no processor pipeline. A processor becomes an augmenter, registered with
Builder::withAugmenters(). It works on theSpecificationand spec attributes rather than on anAnalysisof annotations, so it is a rewrite rather than a rename. Hybrid mode still runs processors added throughBuilder::withGenerator(), but only over the annotations as scanned. - Configuration keys. Keys are named after the processor or augmenter they configure, so they change with the pipeline: classic's
expandEnums.enumNamesis spec'senums.enumNames.openapi --mode <mode> -D <path>prints every key a mode accepts. - Unreferenced components. Spec mode removes components nothing references. Classic's equivalent,
cleanUnusedComponents.enabled, is off by default. A component declared for use outside any path, such as a schema your client code is generated from, needscleanup.enabledset tofalseto survive.
Comparing the documents ​
Generate in spec mode at the version you pinned and compare with the classic document. Some differences are expected and harmless. Other differences lists them. Anything else is either a construct from the list above, or worth an issue.