PHP OpenAPI/Swagger generator using AST, PHPDoc and PHP 8 Attributes
A framework-agnostic PHP Swagger generator that uses static analysis (AST) and PHPDoc. This AST-based API documentation library scans your source code and serves as a powerful OpenAPI 3.1 generator (as well as 3.0). It also features seamless Laravel/Symfony integration out-of-the-box.
- AST-based Static Analysis: No need to run your application.
- Modern PHP Support: Handles namespaces, use aliases, and complex types.
- Global API Metadata Discovery: Automatically extracts
@title,@version,@description,@contact.*,@license.*,@host, and@serverfrom any file. - Security & Authentication: Define global security schemes (ApiKey, JWT) and apply them to endpoints or globally.
- Comprehensive Schema Validation: Support for
minimum,maximum,minLength,maxLength,pattern,format, andexampledirectly in PHPDoc. - Auto-inference: Automatically resolve route parameters and request bodies from method signatures.
- Intelligent Schema Inference: Automatically determines
requiredfields for Model schemas based on PHP native type-hint nullability, PHPDoc types, and default values. Override with explicit@requiredtag. - Advanced Type Resolution:
- Primitives:
int,string,bool,float. - Nullable types:
?stringorstring|null. - Array types:
User[]orarray<User>. - Map/Dictionary types:
array<string, User>(resolves to an object withadditionalPropertiesmapping toUser). - Class references: Automatically resolves FQCN and creates schemas.
- Primitives:
- Advanced OOP Support:
- Inheritance: Properties from parent classes are automatically merged into child schemas.
- Traits: Supports
use Traitwith property merging. - Overrides: Child classes can override parent property types and descriptions.
- Powerful Generics:
- Supports
@templatein class docblocks. - Handles nested generics like
ApiResponse<Collection<User>>. - Supports generic inheritance (e.g.,
class UserResponse extends ApiResponse<User>). - Uses clean schema naming:
ApiResponse.User.
- Supports
- OpenAPI 3.0 & 3.1: Supports both versions, with automatic conversion of nullable types for 3.1.
- Schema Registry: Handles circular references and avoids duplicate definitions.
- Native PHP Enum Support (PHP 8.1+): Automatically extracts enum cases and types for both
BackedEnum(string/int) andUnitEnum. - PHP 8+ Attributes Support: Declare routing, schema, and parameter metadata directly using native PHP 8 attributes (e.g.,
#[Get],#[Property],#[QueryParam],#[Response]). - OpenAPI Linter & Validator: Detect specification integrity issues and unresolved model references with the
--validateoption. - Framework Integrations: Seamless bridges for Laravel Service Providers and Symfony Bundle DI configurations.
composer require phpswag/phpswaguse PhpSwag\Core;
$core = new Core();
$core->setOpenApiVersion('3.1.0'); // Optional, defaults to 3.0.0
// Optional: Enable caching to speed up consecutive generations
$core->enableCache('./.phpswag-cache');
$yaml = $core->generate(['./src/App']);
file_put_contents('swagger.yaml', $yaml);You can define your API information in a top-level PHPDoc block in any of your scanned files:
/**
* @title My Awesome API
* @version 2.1.0
* @description This is a sample API for testing global metadata.
* @contact.name John Doe
* @contact.email john@example.com
* @license.name MIT
* @license.url https://opensource.org/licenses/MIT
* @host https://api.example.com
* @server https://api.production.com Production Server
* @server https://api.staging.com Staging Server
*
* @tag.name Auth Authentication endpoints
* @tag.name Users User management endpoints
*/- Multiple Servers: You can define multiple servers using
@server [URL] [Description]. If@servertags are defined, they will take precedence over@host. - Global Tag Ordering: Explicitly define tags using
@tag.name [name] [description]at the global level. The generated OpenAPI spec will preserve the order and descriptions of these tags. Any other tags found on endpoints that are not declared at the global level will be sorted alphabetically and appended to the end of the list.
You can declare @tag, @security, @accept (or @consume), and @produce at the Controller (class) level. These act as defaults for all methods in the class:
/**
* @tag Users
* @security BearerAuth
* @accept json
* @produce json
*/
class UserController {
/**
* @route POST /users
* @body UserCreateRequest
*/
public function create() {} // Inherits @tag Users, @security BearerAuth, @accept json, @produce json
/**
* @route GET /users/public
* @security
*/
public function listPublic() {} // Overrides security to "no security" (empty array)
}- Multiple/Comma-separated Tags: You can specify multiple tags on a single line separated by commas, e.g.,
@tag Auth, Users(supported at both class and method level). Class-level and method-level tags are automatically merged. - Overrides: Method-level
@security,@accept/@consume, and@producecompletely override the class-level defaults. An empty@securitytag on a method overrides class security to disable authentication for that endpoint.
Define security schemes and requirements globally or per operation:
/**
* @title Security API
* @securityDefinitions.apikey MyApiKey header X-API-KEY
* @securityDefinitions.jwt MyJwtAuth
* @securityDefinitions.basic MyBasicAuth
* @security MyJwtAuth
*/
class Controller {
/**
* @route GET /private
* @security MyApiKey
*/
public function secureAction() {}
/**
* @route GET /scoped
* @security MyJwtAuth[read, write]
*/
public function scopedAction() {}
}- OR logic: Use multiple
@securitytags on a method. - AND logic: Use a single tag with comma-separated schemes:
@security Key1, Key2.
You can add validation constraints and metadata directly in the description of @property, @var, @query, @path, etc., using a simple function-like syntax.
/**
* @query int $age User age minimum(18) maximum(100) default(20)
* @query string $email User email format(email) example(user@example.com)
* @query string $code Auth code pattern(^[A-Z0-9]{6}$) minLength(6) maxLength(6)
*/Supported constraints:
- Numeric:
minimum(n),maximum(n) - String:
minLength(n),maxLength(n),pattern(regex),format(type) - Common:
enum(a,b,c),default(value),example(value)
Values are automatically cast to their appropriate types (integers, floats, or strings) in the final OpenAPI output.
Validation constraints and formats are also fully supported on the @body tag description (e.g., @body string file to upload format(binary)).
The library automatically infers required fields for your component schemas based on properties' PHP type-hints, default values, and PHPDocs.
class User {
/** @var string $id */
public string $id; // Required (non-nullable native type, no default)
/** @var string $name */
public string $name = 'Anonymous'; // Optional (has default value)
/** @var string $email */
public ?string $email; // Optional (nullable native type)
/** @var string $bio */
public string|null $bio; // Optional (nullable union type)
/** @var mixed $extra */
public mixed $extra; // Optional (mixed type can be null)
/** @var string $status */
public $status; // Required (no native type, but @var type is non-nullable string)
/** @var string|null $avatar */
public $avatar; // Optional (no native type, but @var type is nullable string)
}You can override the automatic inference using the @required tag:
- For member properties (class properties):
Use
@requiredas a standalone tag or inline in the description:class User { /** * @var string $email * @required */ public ?string $email; // Required because of explicit @required tag /** @var string $name Name @required */ public ?string $name; // Required because of inline @required /** * @var string $status * @required false */ public string $status; // Optional because of explicit @required false }
- For class-level
@propertydefinitions: Use@required $propertyNamein the class docblock or inline@required:/** * @property string $name @required * @property string $email * @property string $status * * @required $email * @required $status false */ class User {}
The library fully supports PHP 8.1+ native Enums (both BackedEnum and UnitEnum). When an enum is referenced as a type in @response, @property, @var, etc., it is automatically detected and registered in the OpenAPI schemas.
- BackedEnum (string/int): Automatically resolves the schema
typetostringorintegerbased on the backing type, and populates theenumarray with the backing values of all cases. - UnitEnum: Resolves the schema
typetostringand populates theenumarray with the names of all cases.
namespace App\Enums;
// Backed Enum (string)
enum UserStatus: string {
case Pending = 'pending';
case Active = 'active';
case Suspended = 'suspended';
}
// Backed Enum (int)
enum UserRole: int {
case Admin = 1;
case Editor = 2;
case User = 3;
}
// Pure Unit Enum
enum TicketPriority {
case Low;
case Medium;
case High;
}When you use these enums as property types or response types, they are generated in the OpenAPI specification as:
components:
schemas:
App_Enums_UserStatus:
type: string
enum:
- pending
- active
- suspended
App_Enums_UserRole:
type: integer
enum:
- 1
- 2
- 3
App_Enums_TicketPriority:
type: string
enum:
- Low
- Medium
- HighThe generator includes a built-in TypeMappingRegistry that automatically maps common PHP and library classes to their standard OpenAPI representations without requiring you to document them or triggering unresolved class errors:
- DateTime / Date:
DateTime,DateTimeImmutable,DateTimeInterfacemap tostringwithformat: date-time.
- File Uploads:
Symfony\Component\HttpFoundation\File\UploadedFile,Psr\Http\Message\UploadedFileInterface,Illuminate\Http\UploadedFilemap tostringwithformat: binary.
- UUIDs:
Ramsey\Uuid\Uuid,Ramsey\Uuid\UuidInterface,Symfony\Component\Uid\Uuidmap tostringwithformat: uuid(only if the classes/interfaces exist in your runtime environment).
You can register your own custom class-to-schema mappings programmatically using getTypeMappingRegistry():
use PhpSwag\Core;
$core = new Core();
$core->getTypeMappingRegistry()->register(
'App\ValueObjects\Money',
[
'type' => 'number',
'format' => 'money'
]
);The library supports explicit tags and auto-inference (inspired by swaggo).
/**
* @route GET /users/{id}
* @path int $id User unique ID
* @query string $status Filter by status enum(active,inactive) default(active)
*/
public function show(int $id, string $status) {}- Explicit Tags:
@path,@query,@header,@cookie,@body. - Metadata: Support
enum(a,b,c)anddefault(value)in descriptions. - Auto-inference: If no tags are provided, parameters are inferred from the method signature. Primitive types match path/query, and class types match the request body.
You can document your endpoints and models using modern PHP 8+ Attributes instead of (or alongside) PHPDoc comments. Attributes offer IDE autocomplete, static checking, and clean syntax.
All attributes are located under the PhpSwag\Attributes namespace:
- Routing & Tags:
#[Route(method: string, path: string)]or method shortcuts:#[Get(path)],#[Post(path)],#[Put(path)],#[Delete(path)].#[Tag(name: string, description: ?string = null)](Repeatable, can be on class/method).#[OperationId(id: string)](On method).#[Deprecated](On method).
- Parameters & Body:
#[QueryParam],#[PathParam],#[HeaderParam],#[CookieParam]: Specify custom path, query, header, or cookie parameters.#[RequestBody(type: string, description: ?string = null, ...validation)]: Define endpoint's request body schema.
- Response & Schema:
#[Response(code: int|string, type: string, description: ?string = null)](Repeatable, on method).#[Schema(title: ?string = null, description: ?string = null)](On class).#[Property(name: ?string = null, type: ?string = null, description: ?string = null, ...validation, required: ?bool = null)](Repeatable on class, or single on class property).
use PhpSwag\Attributes\Get;
use PhpSwag\Attributes\Tag;
use PhpSwag\Attributes\QueryParam;
use PhpSwag\Attributes\Response;
use PhpSwag\Attributes\Property;
use PhpSwag\Attributes\Schema;
#[Schema(description: "User Response Model")]
class User {
#[Property(description: "User unique ID", minimum: 1)]
public int $id;
#[Property(description: "User email", format: "email")]
public string $email;
}
#[Tag("Users")]
class UserController {
#[Get("/users/{id}")]
#[QueryParam("status", type: "string", description: "Filter by status", enum: ["active", "inactive"])]
#[Response(200, User::class, description: "Success response")]
public function show(int $id) {}
}When both PHPDoc and PHP 8 Attributes are present:
- Single-value properties (e.g.,
summary,description,operationId): Values in Attributes override PHPDoc. PHPDoc is used as a fallback if not declared in Attributes. - Collections (e.g., tags, security): Values from both sources are merged together.
- Keyed Collections (e.g., query params with matching names, response codes): Attributes override PHPDoc for that specific key/parameter. Unmatched keys from both sources are merged.
- Global Metadata:
@title [TEXT]@version [TEXT]@description [TEXT]@contact.name [TEXT]@contact.email [TEXT]@contact.url [TEXT]@license.name [TEXT]@license.url [TEXT]@host [URL]@server [URL] [Description]
- Security:
@securityDefinitions.apikey [NAME] [IN: header|query|cookie] [KEY_NAME]@securityDefinitions.jwt [NAME]@securityDefinitions.basic [NAME]@security [NAME]or@security [NAME[scopes]](supports OR/AND)
- Endpoints:
@route [METHOD] [PATH](e.g.,@route POST /data)@summary [TEXT]@description [TEXT]@tag [NAME]@accept [MIME_TYPE]or@consume [MIME_TYPE](e.g.json,xml, or full MIME type)@produce [MIME_TYPE](e.g.json, xmlor full MIME type)@path [TYPE] $[NAME] [DESC]@query [TYPE] $[NAME] [DESC]@header [TYPE] $[NAME] [DESC]@cookie [TYPE] $[NAME] [DESC]@body [TYPE] [DESC]@response [CODE] [TYPE] [DESC](e.g.,@response 200 ApiResponse<User[]> Success response, supportsdefaultcode)@success [CODE] [TYPE] [DESC](Alias of@response, e.g.,@success 200 User Success, supportsdefaultcode)@failure [CODE] [TYPE] [DESC](Alias of@response, e.g.,@failure 400 ErrorResponse Bad Request, supportsdefaultcode)@operationId [TEXT](Define explicit operationId)@deprecated(Mark the operation as deprecated)@x-[EXTENSION_NAME] [VALUE](Custom OpenAPI extensions, e.g.@x-code-samples [{"lang": "PHP"}])
- Models:
@property [TYPE] $[NAME] [DESCRIPTION](Supports validation tags in description)@var [TYPE](Supports validation tags in description) (for class properties)@required(for member properties) or@required [PROPERTY_NAME] [true|false](for class-level properties or overrides)@template [NAME](for generics)@extends [TYPE]or@use [TYPE](for generic arguments)
To run the unit tests:
composer install
./vendor/bin/phpunitTo run the example generator:
php examples/generate.phpYou can use the CLI to generate documentation without writing any PHP code.
To easily set up a configuration file for your project, run:
./vendor/bin/phpswag initThis starts an interactive wizard that asks for your project options and generates a phpswag.yaml file in your root folder.
If you have a phpswag.yaml file in your root directory, you can simply run:
./vendor/bin/phpswag generateOr, specify options on the command line (which will override values in the configuration file):
./vendor/bin/phpswag generate --path src/Controllers --path src/Models --output swagger.yamlOptions:
--path,-p: Path(s) to scan (can be used multiple times). Supports individual files or directories.--output,-o: Output file path (defaults to stdout).--format,-f: Output format (yamlorjson). Default:yaml.--openapi-version: OpenAPI version (3.0.0or3.1.0). Default:3.0.0.--filter-unused: Filter out schemas that are not referenced by any route.--title: API Title override.--api-version: API Version override.--description: API Description override.--host: API Host/Server URL override.--cache: Enable caching to speed up generation.--cache-file: Cache file path. Default:./.phpswag-cache.--validate: Run validation and linter checks on the generated specification (checks for missing title/version, structural integrity, and unresolved$refschemas).
You can launch a built-in preview server that hosts Swagger UI and hot-reloads instantly when you modify your PHP code:
./vendor/bin/phpswag watchOptions:
--path,-p: Path(s) to scan.--output,-o: Output destination file path (default:swagger.yaml).--format,-f: Output format (yamlorjson).--host: Server host (default:localhost).--port: Server port (default:8080).
An example phpswag.yaml file:
paths:
- src/Controllers
- src/Models
openapi_version: 3.1.0
format: yaml
output: public/swagger.yaml
filter_unused: true
cache: true
cache_file: ./.phpswag-cacheIn addition to PHPDoc annotations, phpswag fully supports native PHP 8 Attributes. Attributes can be used side-by-side with PHPDoc annotations and follow a Smart Merge & Override strategy:
- Single-value metadata (e.g.
summary,description, etc.) defined in Attributes will override PHPDoc definitions. - Parameter definitions are matched by name; Attributes override PHPDoc definitions for the same parameter name.
- Collection tags (e.g.
@tag,@security) defined in both places are merged.
use PhpSwag\Attributes\Get;
use PhpSwag\Attributes\Tag;
use PhpSwag\Attributes\QueryParam;
use PhpSwag\Attributes\Response;
use PhpSwag\Attributes\Schema;
use PhpSwag\Attributes\Property;
#[Tag("Users")]
class UserController {
#[Get("/users/{id}")]
#[QueryParam("status", type: "string", description: "Filter by user status", enum: ["active", "inactive"])]
#[Response(200, User::class, description: "Returns the requested user")]
public function show(int $id) {}
}
#[Schema(title: "User", description: "User representation")]
class User {
#[Property(description: "Unique identifier")]
public int $id; // Native type hint 'int' is automatically inferred as 'integer'!
#[Property(description: "User email address", format: "email")]
public string $email;
}phpswag includes out-of-the-box integrations for Laravel and Symfony.
The Laravel bridge registers config, Artisan commands, and automatic Swagger UI route mappings.
Add the Service Provider in config/app.php (if not auto-discovered):
'providers' => [
// ...
PhpSwag\Bridges\Laravel\PhpSwagServiceProvider::class,
];Publish the configuration file:
php artisan vendor:publish --tag=phpswag-configThis generates config/phpswag.php where you can customize directories to scan, output path, API metadata, and Swagger UI routes.
Run the Artisan command to generate the spec:
php artisan phpswag:generatePass the --validate flag to validate schema references and spec completeness:
php artisan phpswag:generate --validateThe Symfony bridge provides a Bundle to load parameters into the Dependency Injection container and registers Symfony console commands.
Register the bundle in config/bundles.php:
return [
// ...
PhpSwag\Bridges\Symfony\PhpSwagBundle::class => ['all' => true],
];Create a configuration file config/packages/phpswag.yaml:
phpswag:
paths:
- '%kernel.project_dir%/src/Controller'
- '%kernel.project_dir%/src/Entity'
output: '%kernel.project_dir%/public/swagger.yaml'
title: 'My Symfony API'
version: '1.0.0'Run the console command:
php bin/console phpswag:generateTo validate the schema:
php bin/console phpswag:generate --validateWe welcome contributions from the community! If you'd like to help improve phpswag, please review our Contributing Guidelines.
Please also adhere to our Code of Conduct to keep our community approachable and respectable.
For security vulnerabilities, please refer to our Security Policy.
⭐️ Star this repository
If you find phpswag useful, please consider giving it a star on GitHub! It helps the project grow and reach more developers.
☕ Buy me a coffee
This project is developed and maintained entirely for free during my spare time. If you would like to financially support its development, you can buy me a coffee through the following channels:
- PayPal: paypal.me/tolawho
- Ko-fi: ko-fi.com/tolawho
- Crypto (ETH/ERC20):
0x730c98ef98c660862baedef292ae3a041a905cdd - Crypto (BTC):
bc1qv2cp6mcjcd3m2v6hvk09qjg47tljd6mkea6dmuevhxm3a6ps38jqxtwgex