Skip to content

Upgrade to swagger-php v6 and infer more of the spec from Symfony - #5

Merged
yceruto merged 2 commits into
mainfrom
swagger-php-v6
Oct 6, 2026
Merged

yceruto merged 2 commits into
mainfrom
swagger-php-v6

Conversation

@yceruto

@yceruto yceruto commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Upgrade zircote/swagger-php from ^4.7 to ^6.12, resolve the deprecations, and infer more of the OpenAPI spec from Symfony attributes, validator constraints and PHP reflection.

Upgrade and migration

  • zircote/swagger-php ^6.12, phpunit/phpunit ^11.5|^12.5|^13.0, new symfony/type-info requirement, symfony/intl as dev dependency.
  • The generator uses the non-static v6 API.
  • Property, Path and the route attributes pass named arguments to swagger-php (v6 added new constructor parameters).
  • New OpenSolid\OpenApiBundle\OpenApi\Processor\ProcessorInterface, autoconfigured with the openapi.processor tag (swagger-php removed its own interface in v5).
  • The processor pipeline follows the v6 default order.

Deprecations resolved

  • swagger-php: Generator::isDefault() / Generator::UNDEFINED → Undefined; DocBlockAnnotationFactory removed.
  • Symfony 8.1: ViewEvent::$controllerArgumentsEvent, PartialDenormalizationException::getErrors(), defaultPriorityMethod on tagged iterators.
  • PHP 8.5: SplObjectStorage::detach().

New features

  • Native #[MapRequestPayload] (with type:), #[MapQueryString] (with key:) and #[MapQueryParameter] in the spec.
  • Query parameters infer type, format, items, enum, default, description, required and array style.
  • Symfony Validator constraints become schema keywords (formats, Length, Count, Range, comparisons, Regex, Choice, All).
  • Schemas infer required, backed and pure enums, and promoted constructor defaults.
  • Declared responses are merged with the inferred success response; secured operations get 401 and 403.
  • statusCode: option on the route attributes, for the spec and the response.
  • #[Head] and #[Options]; one controller class can declare several HTTP methods.
  • Array items from @return list<View> and @param list<Item> docblocks.

Bug fixes

  • The required default of #[Path] was never applied.
  • #[Path(format: ...)] was written on the parameter object instead of its schema.
  • An empty array result returned 204 instead of 200 with [].
  • An invalid enum value in a payload caused a TypeError.
  • The payload resolver ignored the native type: and key: options.
  • Reading an unknown property on a route attribute was a fatal error.

Behavior changes

  • Object schemas declare their required properties.
  • Query parameters declare a typed schema with default instead of example.
  • Docblock @OA annotations are not read anymore.

See the "Upgrade to swagger-php v6" section in the README.

Tests

  • 158 tests, 100% line, method and class coverage.
  • Green locally on PHP 8.5 / Symfony 8.1, and in Docker on PHP 8.2 with the lowest (Symfony 7.4.0, PHPUnit 11.5) and highest dependencies.
  • CI matrix: PHP 8.2–8.5, plus a lowest-dependencies job.

🤖 Generated with Claude Code

Upgrade zircote/swagger-php to ^6.12 and PHPUnit to ^11.5|^12.5|^13.0,
and resolve the swagger-php, Symfony 8.1 and PHP 8.5 deprecations.

Port the OpenAPI inference of the api library:
- native #[MapRequestPayload], #[MapQueryString] and #[MapQueryParameter]
- query parameter type, format, items, enum, default, description and required
- Symfony Validator constraints as schema keywords
- required fields, enums and promoted constructor defaults on schemas
- merge of declared and inferred responses, 401/403 for secured operations
- statusCode option, #[Head] and #[Options], items from @return docblocks

Fix the Path "required" default, the Path format placement, the 204 for
empty arrays, multi-method controllers and the runtime "type"/"key" options.

Add unit and functional tests for 100% line coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T11:05:37.121206Z 527ddc7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 527ddc7aa9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Attribute/Payload.php
Comment on lines +32 to +34
bool $mapWhenEmpty = false,
) {
parent::__construct($acceptFormat, $serializationContext, $validationGroups, $resolver, $validationFailedStatusCode);
parent::__construct($acceptFormat, $serializationContext, $validationGroups, $resolver, $validationFailedStatusCode, $type, $mapWhenEmpty);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor mapWhenEmpty in the custom payload resolver

Adding mapWhenEmpty here exposes an option that the bundle's RequestPayloadArrayResolver never reads: its mapRequestPayload() still returns null unconditionally for an empty body, after which a required argument produces a 422. Thus #[Payload(mapWhenEmpty: true)] (and native #[MapRequestPayload(mapWhenEmpty: true)], which this resolver also intercepts) does not get Symfony's requested empty-payload mapping behavior.

Useful? React with 👍 / 👎.

Comment thread src/OpenApi/Analyser/Guesser/Operation/OperationResponseGuesser.php Outdated
Comment on lines +90 to +93
if ('path' !== $parameter->in
&& (Undefined::isDefault($parameter->required) || ($parameter->required && $isOptional))
) {
$parameter->required = !$isOptional;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve explicitly required query parameters

For a nullable or defaulted DTO property annotated with #[QueryParameter(required: true)], this condition treats the explicit true as inferred noise and overwrites it with false. Unless a separate NotNull/NotBlank constraint later restores it, the generated contract silently loses the author's explicit required flag; inference should only fill an undefined value or distinguish generated values from declared ones.

Useful? React with 👍 / 👎.

Comment on lines +78 to +82
if (Undefined::isDefault($schema->default) && $hasDefault) {
$default = $reflector->getDefaultValue();
if (null !== $default) {
$schema->default = $default;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize enum defaults before storing them in schemas

When a newly inferred query parameter has a PHP enum default, this assigns the enum object directly to the OpenAPI schema. In particular, a pure enum default such as Kind::Foo cannot be JSON-encoded, so generating /openapi.json fails rather than emitting the case name; convert backed enums to their value and pure enums to their name as AugmentSchemas already does.

Useful? React with 👍 / 👎.

Comment on lines +35 to +36
if (($annotation->_context->reflector ?? null) instanceof \ReflectionProperty) {
$analysis->annotations->offsetUnset($annotation);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve named reusable query-parameter components

This removes every top-level query-parameter attribute attached to a property, not only unnamed parameters belonging to a MapQueryString DTO. A legitimate property-level #[QueryParameter(parameter: 'Filter', ...)] is therefore discarded before MergeIntoComponents, leaving any #/components/parameters/Filter reference unresolved; restrict the removal to parameters that are actually regenerated for mapped DTOs or at least exclude named component declarations.

Useful? React with 👍 / 👎.

…enEmpty

- Merge the inferred body into the first declared 2xx response, which is
  the status code ControllerResultSubscriber sends at runtime, instead of
  adding an unused 200 next to it
- Store backed enum defaults of query parameters as their value and pure
  enum defaults as their name, so the spec can be JSON-encoded
- Keep an explicit required: true on query parameters backed by a property
- Honor mapWhenEmpty (Symfony 8.1+) for #[MapRequestPayload] and
  #[MapQueryString] in RequestPayloadArrayResolver

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yceruto
yceruto merged commit 13e200e into main Oct 6, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant