Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ permissions:

jobs:
tests:
name: Build
name: PHP ${{ matrix.php }} (${{ matrix.dependencies }} dependencies)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- 8.2
- 8.3
- 8.4
- '8.2'
- '8.3'
- '8.4'
- '8.5'
dependencies:
- highest
include:
- php: '8.2'
dependencies: lowest

steps:
- name: Set up PHP
Expand All @@ -27,24 +34,18 @@ jobs:
php-version: ${{ matrix.php }}
extensions: bcmath, gmp, intl, dom, mbstring
ini-values: zend.assertions=1
coverage: none

- name: Checkout code
uses: actions/checkout@v4

- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Validate composer.json
run: composer validate --strict --no-check-lock

- name: Install dependencies
run: composer install --classmap-authoritative
uses: ramsey/composer-install@v3
with:
dependency-versions: ${{ matrix.dependencies }}

- name: Run tests
run: composer test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
vendor/
composer.lock
.phpunit.cache
.phpunit.result.cache
.php-cs-fixer.cache
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,56 @@ class PostProductAction

## Main Features

- [x] Generate OpenAPI spec from PHP attributes (Mainly based on `zircote/swagger-php`)
- [x] Generate OpenAPI spec from PHP attributes (based on `zircote/swagger-php` v6)
- Automatic `Operation`, `Schema` and `Property` guessing from PHP classes and methods
- [x] Expose Swagger UI to explore the OpenAPI spec and test API endpoints
- [x] Export OpenAPI spec in JSON or YAML format (via HTTP and console command)
- [x] Import OpenAPI spec in JSON or YAML format (via config file)
- [x] Define Symfony routes and OpenAPI Paths using the same attributes:
- `#[Post]`, `#[Get]`, `#[Put]`, `#[Patch]`, `#[Delete]`
- `#[Get]`, `#[Post]`, `#[Put]`, `#[Patch]`, `#[Delete]`, `#[Head]`, `#[Options]`
- One controller class can declare many of them
- [x] Conditional OpenAPI Path/Route definition:
- Example: `#[Get('/me', when: 'service("toggle_me").isEnabled()')]`
- [x] Native Symfony mapping attributes:
- `#[MapRequestPayload]` (including `type: Item::class` for arrays) becomes the request body
- `#[MapQueryString]` (including `key: 'filter'`) and `#[MapQueryParameter]` become query parameters
- [x] Symfony attributes abbreviations:
- `#[Payload]` instead of `#[MapRequestPayload]`
- `#[Query]` instead of `#[MapQueryString]`
- [x] OpenAPI attributes abbreviations:
- `#[Path]` instead of `#[PathParameter]`
- `#[Param]` instead of `#[QueryParameter]`
- [x] Symfony's validation constraints definition using OpenAPI attributes:
- [x] Query and path parameter inference from PHP reflection:
- `type`, `format`, `items`, `enum`, `default`, `required` and the array `style`
- `description` from the property or method docblock
- [x] Schema inference from PHP reflection:
- `required` from nullability, default values and the `NotNull` / `NotBlank` constraints
- `type` and `enum` from backed and pure PHP enums
- `default` from promoted constructor parameters
- [x] Symfony Validator constraints describe the spec:
- `Length`, `Count`, `Range`, comparisons, `Regex`, `Choice`, `All`, formats (`Uuid`, `Email`, `Url`, ...) and more
- [x] OpenAPI attributes define Symfony Validator constraints:
- Example: `#[Property(minLength: 3, maxLength: 255)]`
- [x] Response inference from the controller return type:
- Item types of arrays from `@return list<View>` (or the `itemsType` option)
- Declared `responses` are merged with the inferred success response
- `401` and `403` responses for operations with `security`
- Custom success status code with `statusCode: 202`, for the spec and the response
- [x] Automatic controller response serialization (JSON format by default)
- [ ] Generate new endpoints from API spec (WIP) (based on `open-solid/open-api-assistant-bundle`)

## Upgrade to swagger-php v6

- The bundle requires `zircote/swagger-php` ^6.12 and `symfony/type-info`.
- Docblock annotations (`@OA\...`) are not read anymore. Use PHP attributes.
- Custom processors implement `OpenSolid\OpenApiBundle\OpenApi\Processor\ProcessorInterface`.
They are tagged `openapi.processor` automatically. The `OpenApi\Processors\ProcessorInterface`
of swagger-php does not exist anymore.
- The `format` and `enum` options of `#[Path]` now go into the parameter `schema`.
- Object schemas now declare their `required` properties.
- Query parameters now declare their `schema`, with the `default` value instead of an `example`.
- An empty array result now returns `200` with `[]`, not `204`.

## License

This software is published under the [MIT License](LICENSE)
15 changes: 10 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
"symfony/property-access": "^7.4|^8.0",
"symfony/serializer": "^7.4|^8.0",
"symfony/validator": "^7.4|^8.0",
"zircote/swagger-php": "^4.7",
"symfony/expression-language": "^7.4|^8.0"
"zircote/swagger-php": "^6.12",
"symfony/expression-language": "^7.4|^8.0",
"symfony/type-info": "^7.4|^8.0"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"phpunit/phpunit": "^10.0",
"phpunit/phpunit": "^11.5|^12.5|^13.0",
"symfony/browser-kit": "^7.4|^8.0",
"friendsofphp/php-cs-fixer": "^3.54"
"friendsofphp/php-cs-fixer": "^3.54",
"symfony/intl": "^7.4|^8.0"
},
"autoload": {
"psr-4": {
Expand All @@ -38,8 +40,11 @@
}
},
"scripts": {
"test": "vendor/bin/phpunit tests",
"test": "vendor/bin/phpunit",
"psalm": "vendor/bin/psalm --no-cache",
"cs": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
},
"suggest": {
"symfony/intl": "To validate the \"currency\" and \"locale\" formats"
}
}
42 changes: 42 additions & 0 deletions config/openapi/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,36 @@ components:
type: string
type: object
readOnly: true
UnauthorizedErrorView:
properties:
type:
type: string
default: 'https://tools.ietf.org/html/rfc2616#section-10'
title:
type: string
default: 'Unauthorized'
status:
type: integer
default: 401
detail:
type: string
type: object
readOnly: true
ForbiddenErrorView:
properties:
type:
type: string
default: 'https://tools.ietf.org/html/rfc2616#section-10'
title:
type: string
default: 'Forbidden'
status:
type: integer
default: 403
detail:
type: string
type: object
readOnly: true
NotFoundErrorView:
properties:
type:
Expand Down Expand Up @@ -73,6 +103,18 @@ components:
application/json:
schema:
$ref: '#/components/schemas/BadRequestErrorView'
'401':
description: 'Unauthorized'
content:
application/json:
schema:
$ref: '#/components/schemas/UnauthorizedErrorView'
'403':
description: 'Forbidden'
content:
application/json:
schema:
$ref: '#/components/schemas/ForbiddenErrorView'
'404':
description: 'Not found'
content:
Expand Down
2 changes: 1 addition & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
->set('openapi.generator', Generator::class)
->args([
service(AnalyserInterface::class),
tagged_iterator('openapi.processor', defaultPriorityMethod: 'priority'),
tagged_iterator('openapi.processor'),
param('openapi_paths'),
])

Expand Down
6 changes: 0 additions & 6 deletions config/services/openapi/analysers.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@
->set(OABAnalyser\Guesser\Property\PropertyDefaultGuesser::class)
->tag('openapi.analyser_guesser')

->set(OABAnalyser\Guesser\Property\PropertyEnumGuesser::class)
->tag('openapi.analyser_guesser')

->set(OABAnalyser\Guesser\Schema\SchemaDefaultGuesser::class)
->tag('openapi.analyser_guesser')

->set(Analysers\DocBlockAnnotationFactory::class)
->tag('openapi.annotation_factory')

->set(OABAnalyser\Factory\AttributeFactory::class)
->args([
tagged_iterator('openapi.analyser_guesser'),
Expand Down
43 changes: 39 additions & 4 deletions config/services/openapi/processors.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@

return static function (ContainerConfigurator $container): void {
$container->services()
->instanceof(Processors\ProcessorInterface::class)
->tag('openapi.processor')
->set(OAB\Constraint\ConstraintSchemaApplier::class)

->set(Processors\DocBlockDescriptions::class)
->tag('openapi.processor', ['priority' => 100])

->set(Processors\MergeIntoOpenApi::class)
->tag('openapi.processor', ['priority' => 95])

->set(OAB\Processor\RemoveScannedQueryParameters::class)
->tag('openapi.processor', ['priority' => 92])

->set(Processors\MergeIntoComponents::class)
->tag('openapi.processor', ['priority' => 90])

Expand All @@ -36,27 +38,57 @@
->set(Processors\AugmentSchemas::class)
->tag('openapi.processor', ['priority' => 65])

->set(OAB\Processor\AugmentSchemas::class)
->args([service(OAB\Constraint\ConstraintSchemaApplier::class)])
->tag('openapi.processor', ['priority' => 64])

->set(OAB\Processor\AugmentSchemaConstraints::class)
->args([service(OAB\Constraint\ConstraintSchemaApplier::class)])
->tag('openapi.processor', ['priority' => 63])

->set(Processors\AugmentRequestBody::class)
->tag('openapi.processor', ['priority' => 62])

->set(Processors\AugmentProperties::class)
->tag('openapi.processor', ['priority' => 60])

->set(Processors\AugmentDiscriminators::class)
->tag('openapi.processor', ['priority' => 55])

->set(Processors\BuildPaths::class)
->tag('openapi.processor', ['priority' => 50])

->set(Processors\AugmentParameters::class)
->tag('openapi.processor', ['priority' => 45])

->set(OAB\Processor\AugmentPathParameters::class)
->tag('openapi.processor', ['priority' => 44])

->set(OAB\Processor\AugmentQueryParameters::class)
->tag('openapi.processor', ['priority' => 43])

->set(OAB\Processor\AugmentParameterConstraints::class)
->args([service(OAB\Constraint\ConstraintSchemaApplier::class)])
->tag('openapi.processor', ['priority' => 42])

->set(OAB\Processor\AugmentEnumSchemas::class)
->tag('openapi.processor', ['priority' => 41])

->set(Processors\AugmentRefs::class)
->tag('openapi.processor', ['priority' => 40])

->set(OAB\Processor\AugmentSchemas::class)
->tag('openapi.processor', ['priority' => 35])
->set(Processors\AugmentItems::class)
->tag('openapi.processor', ['priority' => 33])

->set(Processors\MergeJsonContent::class)
->tag('openapi.processor', ['priority' => 30])

->set(Processors\MergeXmlContent::class)
->tag('openapi.processor', ['priority' => 25])

->set(Processors\AugmentMediaType::class)
->tag('openapi.processor', ['priority' => 23])

->set(Processors\OperationId::class)
->tag('openapi.processor', ['priority' => 20])

Expand All @@ -72,5 +104,8 @@

->set(OAB\Processor\CleanupAnnotations::class)
->tag('openapi.processor', ['priority' => 5])

->set(Processors\AugmentTags::class)
->tag('openapi.processor', ['priority' => 0])
;
};
36 changes: 4 additions & 32 deletions config/services/routing.php
Original file line number Diff line number Diff line change
@@ -1,43 +1,15 @@
<?php

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use OpenSolid\OpenApiBundle\Routing\Attribute\Delete;
use OpenSolid\OpenApiBundle\Routing\Attribute\Get;
use OpenSolid\OpenApiBundle\Routing\Attribute\Patch;
use OpenSolid\OpenApiBundle\Routing\Attribute\Post;
use OpenSolid\OpenApiBundle\Routing\Attribute\Put;
use OpenSolid\OpenApiBundle\Routing\Attribute\ApiRouteInterface;
use OpenSolid\OpenApiBundle\Routing\Loader\OpenApiRouteControllerLoader;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
$container->services()
->set('routing.loader.attribute.post')
->class(OpenApiRouteControllerLoader::class)
->args(['%kernel.environment%'])
->call('setRouteAttributeClass', [Post::class])
->tag('routing.loader', ['priority' => -5])

->set('routing.loader.attribute.get')
->class(OpenApiRouteControllerLoader::class)
->args(['%kernel.environment%'])
->call('setRouteAttributeClass', [Get::class])
->tag('routing.loader', ['priority' => -5])

->set('routing.loader.attribute.put')
->class(OpenApiRouteControllerLoader::class)
->args(['%kernel.environment%'])
->call('setRouteAttributeClass', [Put::class])
->tag('routing.loader', ['priority' => -5])

->set('routing.loader.attribute.patch')
->class(OpenApiRouteControllerLoader::class)
->args(['%kernel.environment%'])
->call('setRouteAttributeClass', [Patch::class])
->tag('routing.loader', ['priority' => -5])

->set('routing.loader.attribute.delete')
->set('routing.loader.attribute.openapi')
->class(OpenApiRouteControllerLoader::class)
->args(['%kernel.environment%'])
->call('setRouteAttributeClass', [Delete::class])
->call('setRouteAttributeClass', [ApiRouteInterface::class])
->tag('routing.loader', ['priority' => -5])
;
};
Loading
Loading