├── .yarn └── install-state.gz ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── openapi.php ├── renovate.json ├── routes └── api.php ├── src ├── Attributes │ ├── Callback.php │ ├── Collection.php │ ├── Extension.php │ ├── Operation.php │ ├── Parameters.php │ ├── PathItem.php │ ├── RequestBody.php │ ├── Response.php │ └── SecurityRequirement.php ├── Builders │ ├── Components │ │ ├── Builder.php │ │ ├── CallbacksBuilder.php │ │ ├── RequestBodiesBuilder.php │ │ ├── ResponsesBuilder.php │ │ ├── SchemasBuilder.php │ │ └── SecuritySchemesBuilder.php │ ├── ComponentsBuilder.php │ ├── ExtensionsBuilder.php │ ├── InfoBuilder.php │ ├── Paths │ │ ├── Operation │ │ │ ├── CallbacksBuilder.php │ │ │ ├── ParametersBuilder.php │ │ │ ├── RequestBodyBuilder.php │ │ │ ├── ResponsesBuilder.php │ │ │ └── SecurityBuilder.php │ │ └── OperationsBuilder.php │ ├── PathsBuilder.php │ ├── ServersBuilder.php │ └── TagsBuilder.php ├── ClassMapGenerator.php ├── Concerns │ └── Referencable.php ├── Console │ ├── CallbackFactoryMakeCommand.php │ ├── ExtensionFactoryMakeCommand.php │ ├── GenerateCommand.php │ ├── ParametersFactoryMakeCommand.php │ ├── RequestBodyFactoryMakeCommand.php │ ├── ResponseFactoryMakeCommand.php │ ├── RoutesCommand.php │ ├── SchemaFactoryMakeCommand.php │ ├── SecuritySchemeFactoryMakeCommand.php │ └── stubs │ │ ├── callback.stub │ │ ├── extension.stub │ │ ├── parameters.stub │ │ ├── requestbody.stub │ │ ├── response.stub │ │ ├── schema.model.stub │ │ ├── schema.stub │ │ └── securityscheme.stub ├── Contracts │ ├── ComponentMiddleware.php │ ├── PathMiddleware.php │ └── Reusable.php ├── Factories │ ├── CallbackFactory.php │ ├── ExtensionFactory.php │ ├── ParametersFactory.php │ ├── RequestBodyFactory.php │ ├── ResponseFactory.php │ ├── SchemaFactory.php │ ├── SecuritySchemeFactory.php │ └── ServerFactory.php ├── Generator.php ├── Http │ └── OpenApiController.php ├── OpenApiServiceProvider.php ├── RouteInformation.php └── SchemaHelpers.php └── yarn.lock /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nova-Edge/laravel-openapi/64c20987ffcdadf2b4034c565349c0341c14af29/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.1.2](https://github.com/Nova-Edge/laravel-openapi/compare/v2.1.1...v2.1.2) (2025-03-31) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * check if 'actionDocBlock' is null ([#43](https://github.com/Nova-Edge/laravel-openapi/issues/43)) ([033d21c](https://github.com/Nova-Edge/laravel-openapi/commit/033d21ccb221f45ee55d8ec0599d64bb28fff8f0)) 9 | 10 | ## [2.1.1](https://github.com/Nova-Edge/laravel-openapi/compare/v2.1.0...v2.1.1) (2025-02-24) 11 | 12 | 13 | ### Miscellaneous Chores 14 | 15 | * **deps:** Support PHP 8.4 and Laravel 12 ([#39](https://github.com/Nova-Edge/laravel-openapi/issues/39)) ([c1eae1d](https://github.com/Nova-Edge/laravel-openapi/commit/c1eae1d680b1f36579f8a3c416dd6adf7883f3f9)) 16 | 17 | ## [2.1.0](https://github.com/Nova-Edge/laravel-openapi/compare/v2.0.1...v2.1.0) (2025-02-03) 18 | 19 | 20 | ### Features 21 | 22 | * move security requirements to a dedicated attribute ([#34](https://github.com/Nova-Edge/laravel-openapi/issues/34)) ([033c354](https://github.com/Nova-Edge/laravel-openapi/commit/033c354c47cab100d0bbb0354d4f0709e0484379)) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * deprecation warning with Str::lower() ([#36](https://github.com/Nova-Edge/laravel-openapi/issues/36)) ([a92b4ce](https://github.com/Nova-Edge/laravel-openapi/commit/a92b4ce171daa721c94da23e42bf02c4f5896951)) 28 | 29 | ## [2.0.1](https://github.com/Nova-Edge/laravel-openapi/compare/v2.0.0...v2.0.1) (2024-12-09) 30 | 31 | 32 | ### Performance Improvements 33 | 34 | * **memoization:** add memoization for build referencable schemas ([#30](https://github.com/Nova-Edge/laravel-openapi/issues/30)) ([7657892](https://github.com/Nova-Edge/laravel-openapi/commit/765789290c4b43dedf3f7e4f96ef32cbe82a9087)) 35 | 36 | ## [2.0.0](https://github.com/Nova-Edge/laravel-openapi/compare/v1.13.1...v2.0.0) (2024-08-02) 37 | 38 | 39 | ### ⚠ BREAKING CHANGES 40 | 41 | * requires Laravel 11 42 | 43 | ### Miscellaneous Chores 44 | 45 | * remove doctrine ([#17](https://github.com/Nova-Edge/laravel-openapi/issues/17)) ([ae4695e](https://github.com/Nova-Edge/laravel-openapi/commit/ae4695e9973fe6b9e70c4c132bdc324c57075635)) 46 | 47 | ## [1.13.1](https://github.com/Nova-Edge/laravel-openapi/compare/v1.13.0...v1.13.1) (2024-07-13) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * Corrected SchemaFactoryMakeCommand.php to work with Laravel 11 ([#15](https://github.com/Nova-Edge/laravel-openapi/issues/15)) ([b0f57cf](https://github.com/Nova-Edge/laravel-openapi/commit/b0f57cf0b56a0edbe686a0202631e9cfc9b8283a)) 53 | 54 | 55 | ### Miscellaneous Chores 56 | 57 | * **deps:** update actions/setup-node action to v4 ([#7](https://github.com/Nova-Edge/laravel-openapi/issues/7)) ([e53fc5e](https://github.com/Nova-Edge/laravel-openapi/commit/e53fc5e09aadbbc40f63cfa236155e57a23630a6)) 58 | * **deps:** update dependency vuepress to v1.9.10 ([#2](https://github.com/Nova-Edge/laravel-openapi/issues/2)) ([9d80e06](https://github.com/Nova-Edge/laravel-openapi/commit/9d80e069c75cb65fe73a5fcd6f994e3c453a15f7)) 59 | 60 | ## [1.13.0](https://github.com/Nova-Edge/laravel-openapi/compare/v1.12.0...v1.13.0) (2024-04-12) 61 | 62 | 63 | ### Features 64 | 65 | * **deps:** support laravel 11 ([#6](https://github.com/Nova-Edge/laravel-openapi/issues/6)) ([278031d](https://github.com/Nova-Edge/laravel-openapi/commit/278031da0d02bcc6b204ab61390e7e4c91de391d)) 66 | 67 | 68 | ### Miscellaneous Chores 69 | 70 | * **deps:** update actions/checkout action to v4 ([#3](https://github.com/Nova-Edge/laravel-openapi/issues/3)) ([c4b4794](https://github.com/Nova-Edge/laravel-openapi/commit/c4b479401102b88bd6e6ea06ba2610288dbb292d)) 71 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ugo.mignon@gameverse.app. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Vladimir Yuldashev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate OpenAPI specification for Laravel Applications 2 | 3 | ![Packagist Version](https://img.shields.io/packagist/v/tartanlegrand/laravel-openapi?style=flat-square) 4 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | ![GitHub branch status](https://img.shields.io/github/checks-status/TartanLeGrand/laravel-openapi/main?style=flat-square) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/tartanlegrand/laravel-openapi.svg?style=flat-square)](https://packagist.org/packages/tartanlegrand/laravel-openapi) 7 | 8 | ## Documentation 9 | 10 | You'll find the documentation on https://nova-edge.github.io/laravel-openapi. 11 | 12 | ## License 13 | 14 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 15 | 16 | ## Community and Contributing 17 | 18 | We invite you to join our community and contribute to the project. Whether you're interested in development, reporting bugs, or simply providing suggestions and feedback, all contributions are welcome! 19 | 20 | ### How to Contribute 21 | 22 | 1. **Report a Bug**: Open a [GitHub issue](https://github.com/Nova-Edge/laravel-openapi/issues) to report bugs or propose improvements. 23 | 2. **Submit Code**: For development details, see the [CONTRIBUTING](CONTRIBUTING.md) guide. For major changes, including CLI interface changes, please open an issue first to discuss what you would like to change. 24 | 3. **Documentation**: Help us improve the documentation on [our site](https://nova-edge.github.io/laravel-openapi). 25 | 26 | Thanks to everyone who has contributed, including bug reports, code, feedback, and suggestions! 27 | 28 | ### List of Contributors 29 | 30 | A big thank you to all who have contributed to this project. Here are some of our most active contributors: 31 | 32 | 33 | 34 | 35 | 36 | If you would like to see your name here, start contributing today! 37 | 38 | ## Join Us 39 | 40 | Connect with other users and contributors on our [discussion forum](https://github.com/Nova-Edge/laravel-openapi/discussions) and join the conversation around the project. 41 | 42 | We ❤️ contributions big or small. Thank you for being part of our community and helping improve this open source project. 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tartanlegrand/laravel-openapi", 3 | "description": "Generate OpenAPI Specification for Laravel Applications", 4 | "keywords": [ 5 | "laravel", 6 | "openapi", 7 | "api", 8 | "documentation", 9 | "docs", 10 | "rest", 11 | "swagger" 12 | ], 13 | "type": "library", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Vladimir Yuldashev", 18 | "email": "misterio92@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.0", 23 | "ext-json": "*", 24 | "goldspecdigital/oooas": "^2.7.1", 25 | "laravel/framework": "^11.0|^12.0", 26 | "phpdocumentor/reflection-docblock": "^5.0" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "^5.3|^6.0|^7.0|^8.0|^9.0||^10", 30 | "phpunit/phpunit": "^9.5.13|^10.5||^11.5|^12.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Vyuldashev\\LaravelOpenApi\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Vyuldashev\\LaravelOpenApi\\Tests\\": "tests/", 40 | "Examples\\Petstore\\": "examples/petstore/" 41 | } 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Vyuldashev\\LaravelOpenApi\\OpenApiServiceProvider" 50 | ] 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /config/openapi.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | 'default' => [ 8 | 9 | 'info' => [ 10 | 'title' => config('app.name'), 11 | 'description' => null, 12 | 'version' => '1.0.0', 13 | 'contact' => [], 14 | ], 15 | 16 | 'servers' => [ 17 | [ 18 | 'url' => env('APP_URL'), 19 | 'description' => null, 20 | 'variables' => [], 21 | ], 22 | ], 23 | 24 | 'tags' => [ 25 | 26 | // [ 27 | // 'name' => 'user', 28 | // 'description' => 'Application users', 29 | // ], 30 | 31 | ], 32 | 33 | 'security' => [ 34 | // GoldSpecDigital\ObjectOrientedOAS\Objects\SecurityRequirement::create()->securityScheme('JWT'), 35 | ], 36 | 37 | // Non standard attributes used by code/doc generation tools can be added here 38 | 'extensions' => [ 39 | // 'x-tagGroups' => [ 40 | // [ 41 | // 'name' => 'General', 42 | // 'tags' => [ 43 | // 'user', 44 | // ], 45 | // ], 46 | // ], 47 | ], 48 | 49 | // Route for exposing specification. 50 | // Leave uri null to disable. 51 | 'route' => [ 52 | 'uri' => '/openapi', 53 | 'middleware' => [], 54 | ], 55 | 56 | // Register custom middlewares for different objects. 57 | 'middlewares' => [ 58 | 'paths' => [ 59 | // 60 | ], 61 | 'components' => [ 62 | // 63 | ], 64 | ], 65 | 66 | ], 67 | 68 | ], 69 | 70 | // Directories to use for locating OpenAPI object definitions. 71 | 'locations' => [ 72 | 'callbacks' => [ 73 | app_path('OpenApi/Callbacks'), 74 | ], 75 | 76 | 'request_bodies' => [ 77 | app_path('OpenApi/RequestBodies'), 78 | ], 79 | 80 | 'responses' => [ 81 | app_path('OpenApi/Responses'), 82 | ], 83 | 84 | 'schemas' => [ 85 | app_path('OpenApi/Schemas'), 86 | ], 87 | 88 | 'security_schemes' => [ 89 | app_path('OpenApi/SecuritySchemes'), 90 | ], 91 | ], 92 | 93 | ]; 94 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "groupName": "Actions dependencies", 9 | "groupSlug": "actions-minor-patch", 10 | "matchManagers": [ 11 | "github-actions" 12 | ], 13 | "matchUpdateTypes": [ 14 | "minor", 15 | "patch" 16 | ], 17 | "addLabels": [ 18 | "🤖 actions" 19 | ] 20 | }, 21 | { 22 | "groupName": "frontend dependencies", 23 | "groupSlug": "frontend-minor-patch", 24 | "matchManagers": [ 25 | "npm" 26 | ], 27 | "matchUpdateTypes": [ 28 | "minor", 29 | "patch" 30 | ], 31 | "addLabels": [ 32 | "🌐 frontend" 33 | ] 34 | }, 35 | { 36 | "groupName": "backend dependencies", 37 | "groupSlug": "backend-minor-patch", 38 | "matchManagers": [ 39 | "composer" 40 | ], 41 | "matchUpdateTypes": [ 42 | "minor", 43 | "patch" 44 | ], 45 | "addLabels": [ 46 | "🔙 backend" 47 | ] 48 | }, 49 | { 50 | "groupName": "docker dependencies", 51 | "groupSlug": "docker-minor-patch", 52 | "matchManagers": [ 53 | "dockerfile", 54 | "docker-compose" 55 | ], 56 | "matchUpdateTypes": [ 57 | "minor", 58 | "patch" 59 | ], 60 | "addLabels": [ 61 | "🐳 docker" 62 | ] 63 | }, 64 | { 65 | "matchUpdateTypes": [ 66 | "major" 67 | ], 68 | "addLabels": [ 69 | "🚨 major" 70 | ] 71 | }, 72 | { 73 | "matchUpdateTypes": [ 74 | "minor" 75 | ], 76 | "addLabels": [ 77 | "✨ minor" 78 | ] 79 | }, 80 | { 81 | "matchUpdateTypes": [ 82 | "patch" 83 | ], 84 | "addLabels": [ 85 | "🛠️ patch" 86 | ] 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | 'openapi.'], function () { 8 | foreach (config('openapi.collections', []) as $name => $config) { 9 | $uri = Arr::get($config, 'route.uri'); 10 | 11 | if (! $uri) { 12 | continue; 13 | } 14 | 15 | Route::get($uri, [OpenApiController::class, 'show']) 16 | ->name($name.'.specification') 17 | ->middleware(Arr::get($config, 'route.middleware')); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/Attributes/Callback.php: -------------------------------------------------------------------------------- 1 | factory = class_exists($factory) ? $factory : app()->getNamespace().'OpenApi\\Callbacks\\'.$factory; 17 | 18 | if (! is_a($this->factory, CallbackFactory::class, true)) { 19 | throw new InvalidArgumentException('Factory class must be instance of CallbackFactory'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Attributes/Collection.php: -------------------------------------------------------------------------------- 1 | */ 11 | public array|string $name; 12 | 13 | public function __construct(string|array $name = 'default') 14 | { 15 | $this->name = $name; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Attributes/Extension.php: -------------------------------------------------------------------------------- 1 | factory = class_exists($factory) ? $factory : app()->getNamespace().'OpenApi\\Extensions\\'.$factory; 20 | 21 | if (! is_a($this->factory, ExtensionFactory::class, true)) { 22 | throw new InvalidArgumentException('Factory class must be instance of ExtensionFactory'); 23 | } 24 | } 25 | 26 | $this->factory ??= null; 27 | $this->key = $key; 28 | $this->value = $value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Attributes/Operation.php: -------------------------------------------------------------------------------- 1 | */ 15 | public array $tags; 16 | 17 | public ?string $security; 18 | 19 | public ?string $method; 20 | 21 | public ?array $servers; 22 | 23 | /** 24 | * @param string|null $id 25 | * @param array $tags 26 | * @param \Vyuldashev\LaravelOpenApi\Factories\SecuritySchemeFactory|string|null $security Deprecated 27 | * @param string|null $method 28 | * 29 | * @throws InvalidArgumentException 30 | */ 31 | public function __construct(string $id = null, array $tags = [], string $security = null, string $method = null, array $servers = null) 32 | { 33 | $this->id = $id; 34 | $this->tags = $tags; 35 | $this->method = $method; 36 | $this->servers = $servers; 37 | 38 | if ($security !== null) { 39 | @trigger_error('Operation()\'s \'security\' argument is deprecated. Use the SecurityRequirement() attribute instead.', E_USER_DEPRECATED); 40 | } 41 | 42 | if ($security === '') { 43 | //user wants to turn off security on this operation 44 | $this->security = $security; 45 | 46 | return; 47 | } 48 | 49 | if ($security) { 50 | $this->security = class_exists($security) ? $security : app()->getNamespace().'OpenApi\\SecuritySchemes\\'.$security; 51 | 52 | if (! is_a($this->security, SecuritySchemeFactory::class, true)) { 53 | throw new InvalidArgumentException( 54 | sprintf('Security class is either not declared or is not an instance of %s', SecuritySchemeFactory::class) 55 | ); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Attributes/Parameters.php: -------------------------------------------------------------------------------- 1 | factory = class_exists($factory) ? $factory : app()->getNamespace().'OpenApi\\Parameters\\'.$factory; 17 | 18 | if (! is_a($this->factory, ParametersFactory::class, true)) { 19 | throw new InvalidArgumentException('Factory class must be instance of ParametersFactory'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Attributes/PathItem.php: -------------------------------------------------------------------------------- 1 | factory = class_exists($factory) ? $factory : app()->getNamespace().'OpenApi\\RequestBodies\\'.$factory; 17 | 18 | if (! is_a($this->factory, RequestBodyFactory::class, true)) { 19 | throw new InvalidArgumentException('Factory class must be instance of RequestBodyFactory'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Attributes/Response.php: -------------------------------------------------------------------------------- 1 | factory = class_exists($factory) ? $factory : app()->getNamespace().'OpenApi\\Responses\\'.$factory; 21 | 22 | if (! is_a($this->factory, ResponseFactory::class, true)) { 23 | throw new InvalidArgumentException('Factory class must be instance of ResponseFactory'); 24 | } 25 | 26 | $this->statusCode = $statusCode; 27 | $this->description = $description; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Attributes/SecurityRequirement.php: -------------------------------------------------------------------------------- 1 | */ 15 | public ?array $scopes; 16 | 17 | /** 18 | * @param string|null $scheme 19 | * @param array $scopes 20 | * 21 | * @throws InvalidArgumentException 22 | */ 23 | public function __construct(string|null $scheme, array $scopes = []) 24 | { 25 | if ($scheme) { 26 | $factory = class_exists($scheme) ? $scheme : app()->getNamespace().'OpenApi\\SecuritySchemes\\'.$scheme; 27 | 28 | if (! is_a($factory, SecuritySchemeFactory::class, true)) { 29 | throw new InvalidArgumentException( 30 | sprintf('Security class is either not declared or is not an instance of %s', SecuritySchemeFactory::class) 31 | ); 32 | } 33 | } 34 | 35 | $this->scheme = $scheme; 36 | $this->scopes = $scopes; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Builders/Components/Builder.php: -------------------------------------------------------------------------------- 1 | directories = $directories; 18 | } 19 | 20 | protected function getAllClasses(string $collection): Collection 21 | { 22 | return collect($this->directories) 23 | ->map(function (string $directory) { 24 | $map = ClassMapGenerator::createMap($directory); 25 | 26 | return array_keys($map); 27 | }) 28 | ->flatten() 29 | ->filter(function (string $class) use ($collection) { 30 | $reflectionClass = new ReflectionClass($class); 31 | $collectionAttributes = $reflectionClass->getAttributes(CollectionAttribute::class); 32 | 33 | if (count($collectionAttributes) === 0 && $collection === Generator::COLLECTION_DEFAULT) { 34 | return true; 35 | } 36 | 37 | if (count($collectionAttributes) === 0) { 38 | return false; 39 | } 40 | 41 | /** @var CollectionAttribute $collectionAttribute */ 42 | $collectionAttribute = $collectionAttributes[0]->newInstance(); 43 | 44 | return 45 | $collectionAttribute->name === ['*'] || 46 | in_array($collection, $collectionAttribute->name ?? [], true); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Builders/Components/CallbacksBuilder.php: -------------------------------------------------------------------------------- 1 | getAllClasses($collection) 14 | ->filter(static function ($class) { 15 | return 16 | is_a($class, CallbackFactory::class, true) && 17 | is_a($class, Reusable::class, true); 18 | }) 19 | ->map(static function ($class) { 20 | /** @var CallbackFactory $instance */ 21 | $instance = app($class); 22 | 23 | return $instance->build(); 24 | }) 25 | ->values() 26 | ->toArray(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builders/Components/RequestBodiesBuilder.php: -------------------------------------------------------------------------------- 1 | getAllClasses($collection) 14 | ->filter(static function ($class) { 15 | return 16 | is_a($class, RequestBodyFactory::class, true) && 17 | is_a($class, Reusable::class, true); 18 | }) 19 | ->map(static function ($class) { 20 | /** @var RequestBodyFactory $instance */ 21 | $instance = app($class); 22 | 23 | return $instance->build(); 24 | }) 25 | ->values() 26 | ->toArray(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builders/Components/ResponsesBuilder.php: -------------------------------------------------------------------------------- 1 | getAllClasses($collection) 14 | ->filter(static function ($class) { 15 | return 16 | is_a($class, ResponseFactory::class, true) && 17 | is_a($class, Reusable::class, true); 18 | }) 19 | ->map(static function ($class) { 20 | /** @var ResponseFactory $instance */ 21 | $instance = app($class); 22 | 23 | return $instance->build(); 24 | }) 25 | ->values() 26 | ->toArray(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builders/Components/SchemasBuilder.php: -------------------------------------------------------------------------------- 1 | getAllClasses($collection) 14 | ->filter(static function ($class) { 15 | return 16 | is_a($class, SchemaFactory::class, true) && 17 | is_a($class, Reusable::class, true); 18 | }) 19 | ->map(static function ($class) { 20 | /** @var SchemaFactory $instance */ 21 | $instance = app($class); 22 | 23 | return $instance->build(); 24 | }) 25 | ->values() 26 | ->toArray(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Builders/Components/SecuritySchemesBuilder.php: -------------------------------------------------------------------------------- 1 | getAllClasses($collection) 13 | ->filter(static function ($class) { 14 | return is_a($class, SecuritySchemeFactory::class, true); 15 | }) 16 | ->map(static function ($class) { 17 | /** @var SecuritySchemeFactory $instance */ 18 | $instance = app($class); 19 | 20 | return $instance->build(); 21 | }) 22 | ->values() 23 | ->toArray(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Builders/ComponentsBuilder.php: -------------------------------------------------------------------------------- 1 | callbacksBuilder = $callbacksBuilder; 29 | $this->requestBodiesBuilder = $requestBodiesBuilder; 30 | $this->responsesBuilder = $responsesBuilder; 31 | $this->schemasBuilder = $schemasBuilder; 32 | $this->securitySchemesBuilder = $securitySchemesBuilder; 33 | } 34 | 35 | public function build( 36 | string $collection = Generator::COLLECTION_DEFAULT, 37 | array $middlewares = [] 38 | ): ?Components { 39 | $callbacks = $this->callbacksBuilder->build($collection); 40 | $requestBodies = $this->requestBodiesBuilder->build($collection); 41 | $responses = $this->responsesBuilder->build($collection); 42 | $schemas = $this->schemasBuilder->build($collection); 43 | $securitySchemes = $this->securitySchemesBuilder->build($collection); 44 | 45 | $components = Components::create(); 46 | 47 | $hasAnyObjects = false; 48 | 49 | if (count($callbacks) > 0) { 50 | $hasAnyObjects = true; 51 | 52 | $components = $components->callbacks(...$callbacks); 53 | } 54 | 55 | if (count($requestBodies) > 0) { 56 | $hasAnyObjects = true; 57 | 58 | $components = $components->requestBodies(...$requestBodies); 59 | } 60 | 61 | if (count($responses) > 0) { 62 | $hasAnyObjects = true; 63 | $components = $components->responses(...$responses); 64 | } 65 | 66 | if (count($schemas) > 0) { 67 | $hasAnyObjects = true; 68 | $components = $components->schemas(...$schemas); 69 | } 70 | 71 | if (count($securitySchemes) > 0) { 72 | $hasAnyObjects = true; 73 | $components = $components->securitySchemes(...$securitySchemes); 74 | } 75 | 76 | if (! $hasAnyObjects) { 77 | return null; 78 | } 79 | 80 | foreach ($middlewares as $middleware) { 81 | app($middleware)->after($components); 82 | } 83 | 84 | return $components; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Builders/ExtensionsBuilder.php: -------------------------------------------------------------------------------- 1 | filter(static fn (object $attribute) => $attribute instanceof ExtensionAttribute) 16 | ->each(static function (ExtensionAttribute $attribute) use ($object): void { 17 | if ($attribute->factory) { 18 | /** @var ExtensionFactory $factory */ 19 | $factory = app($attribute->factory); 20 | $key = $factory->key(); 21 | $value = $factory->value(); 22 | } else { 23 | $key = $attribute->key; 24 | $value = $attribute->value; 25 | } 26 | 27 | $object->x( 28 | $key, 29 | $value 30 | ); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Builders/InfoBuilder.php: -------------------------------------------------------------------------------- 1 | title(Arr::get($config, 'title')) 16 | ->description(Arr::get($config, 'description')) 17 | ->version(Arr::get($config, 'version')); 18 | 19 | if (Arr::has($config, 'contact') && 20 | ( 21 | array_key_exists('name', $config['contact']) || 22 | array_key_exists('email', $config['contact']) || 23 | array_key_exists('url', $config['contact']) 24 | ) 25 | ) { 26 | $info = $info->contact($this->buildContact($config['contact'])); 27 | } 28 | 29 | if (Arr::has($config, 'license') && array_key_exists('name', $config['license'])) { 30 | $info = $info->license($this->buildLicense($config['license'])); 31 | } 32 | 33 | $extensions = $config['extensions'] ?? []; 34 | 35 | foreach ($extensions as $key => $value) { 36 | $info->x($key, $value); 37 | } 38 | 39 | return $info; 40 | } 41 | 42 | protected function buildContact(array $config): Contact 43 | { 44 | return Contact::create() 45 | ->name(Arr::get($config, 'name')) 46 | ->email(Arr::get($config, 'email')) 47 | ->url(Arr::get($config, 'url')); 48 | } 49 | 50 | protected function buildLicense(array $config): License 51 | { 52 | return License::create() 53 | ->name(Arr::get($config, 'name')) 54 | ->url(Arr::get($config, 'url')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Builders/Paths/Operation/CallbacksBuilder.php: -------------------------------------------------------------------------------- 1 | actionAttributes 15 | ->filter(static fn (object $attribute) => $attribute instanceof CallbackAttribute) 16 | ->map(static function (CallbackAttribute $attribute) { 17 | $factory = app($attribute->factory); 18 | $pathItem = $factory->build(); 19 | 20 | if ($factory instanceof Reusable) { 21 | return PathItem::ref('#/components/callbacks/'.$pathItem->objectId); 22 | } 23 | 24 | return $pathItem; 25 | }) 26 | ->values() 27 | ->toArray(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Builders/Paths/Operation/ParametersBuilder.php: -------------------------------------------------------------------------------- 1 | buildPath($route); 21 | $attributedParameters = $this->buildAttribute($route); 22 | 23 | return $pathParameters->merge($attributedParameters)->toArray(); 24 | } 25 | 26 | protected function buildPath(RouteInformation $route): Collection 27 | { 28 | return collect($route->parameters) 29 | ->map(static function (array $parameter) use ($route) { 30 | $schema = Schema::string(); 31 | 32 | /** @var ReflectionParameter|null $reflectionParameter */ 33 | $reflectionParameter = collect($route->actionParameters) 34 | ->first(static fn (ReflectionParameter $reflectionParameter) => $reflectionParameter->name === $parameter['name']); 35 | 36 | if ($reflectionParameter) { 37 | // The reflected param has no type, so ignore (should be defined in a ParametersFactory instead) 38 | if ($reflectionParameter->getType() === null) { 39 | return null; 40 | } 41 | 42 | $schema = SchemaHelpers::guessFromReflectionType($reflectionParameter->getType()); 43 | } 44 | 45 | /** @var Param $description */ 46 | $description = collect($route->actionDocBlock->getTagsByName('param')) 47 | ->first(static fn (Param $param) => Str::snake($param->getVariableName()) === Str::snake($parameter['name'])); 48 | 49 | return Parameter::path()->name($parameter['name']) 50 | ->required() 51 | ->description(optional(optional($description)->getDescription())->render()) 52 | ->schema($schema); 53 | }) 54 | ->filter(); 55 | } 56 | 57 | protected function buildAttribute(RouteInformation $route): Collection 58 | { 59 | /** @var Parameters|null $parameters */ 60 | $parameters = $route->actionAttributes->first(static fn ($attribute) => $attribute instanceof Parameters, []); 61 | 62 | if ($parameters) { 63 | /** @var ParametersFactory $parametersFactory */ 64 | $parametersFactory = app($parameters->factory); 65 | 66 | $parameters = $parametersFactory->build(); 67 | } 68 | 69 | return collect($parameters); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Builders/Paths/Operation/RequestBodyBuilder.php: -------------------------------------------------------------------------------- 1 | actionAttributes->first(static fn (object $attribute) => $attribute instanceof RequestBodyAttribute); 17 | 18 | if ($requestBody) { 19 | /** @var RequestBodyFactory $requestBodyFactory */ 20 | $requestBodyFactory = app($requestBody->factory); 21 | 22 | $requestBody = $requestBodyFactory->build(); 23 | 24 | if ($requestBodyFactory instanceof Reusable) { 25 | return RequestBody::ref('#/components/requestBodies/'.$requestBody->objectId); 26 | } 27 | } 28 | 29 | return $requestBody; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Builders/Paths/Operation/ResponsesBuilder.php: -------------------------------------------------------------------------------- 1 | actionAttributes 15 | ->filter(static fn (object $attribute) => $attribute instanceof ResponseAttribute) 16 | ->map(static function (ResponseAttribute $attribute) { 17 | $factory = app($attribute->factory); 18 | $response = $factory->build(); 19 | 20 | if ($factory instanceof Reusable) { 21 | return Response::ref('#/components/responses/'.$response->objectId) 22 | ->statusCode($attribute->statusCode) 23 | ->description($attribute->description); 24 | } 25 | 26 | return $response; 27 | }) 28 | ->values() 29 | ->toArray(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Builders/Paths/Operation/SecurityBuilder.php: -------------------------------------------------------------------------------- 1 | actionAttributes 15 | ->filter(static fn (object $attribute) => $attribute instanceof OperationAttribute || $attribute instanceof SecurityRequirementAttribute) 16 | ->filter(static fn (OperationAttribute|SecurityRequirementAttribute $attribute) => $attribute instanceof SecurityRequirementAttribute || isset($attribute->security)) 17 | ->map(static function (OperationAttribute|SecurityRequirementAttribute $attribute) { 18 | if ($attribute instanceof SecurityRequirementAttribute) { 19 | if (!$attribute->scheme) { 20 | return SecurityRequirement::create()->securityScheme(null); 21 | } 22 | $factory = app($attribute->scheme); 23 | $scheme = $factory->build(); 24 | 25 | $requirement = SecurityRequirement::create()->securityScheme($scheme); 26 | 27 | if ($attribute->scopes) { 28 | $requirement = $requirement->scopes(...$attribute->scopes); 29 | } 30 | 31 | return $requirement; 32 | } else { 33 | // return a null scheme if the security is set to '' 34 | if ($attribute->security === '') { 35 | return SecurityRequirement::create()->securityScheme(null); 36 | } 37 | $security = app($attribute->security); 38 | $scheme = $security->build(); 39 | 40 | return SecurityRequirement::create()->securityScheme($scheme); 41 | } 42 | }) 43 | ->values() 44 | ->toArray(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Builders/Paths/OperationsBuilder.php: -------------------------------------------------------------------------------- 1 | callbacksBuilder = $callbacksBuilder; 39 | $this->parametersBuilder = $parametersBuilder; 40 | $this->requestBodyBuilder = $requestBodyBuilder; 41 | $this->responsesBuilder = $responsesBuilder; 42 | $this->extensionsBuilder = $extensionsBuilder; 43 | $this->securityBuilder = $securityBuilder; 44 | } 45 | 46 | /** 47 | * @param RouteInformation[]|Collection $routes 48 | * @return array 49 | * 50 | * @throws InvalidArgumentException 51 | */ 52 | public function build(array|Collection $routes): array 53 | { 54 | $operations = []; 55 | 56 | /** @var RouteInformation[] $routes */ 57 | foreach ($routes as $route) { 58 | /** @var OperationAttribute|null $operationAttribute */ 59 | $operationAttribute = $route->actionAttributes 60 | ->first(static fn(object $attribute) => $attribute instanceof OperationAttribute); 61 | 62 | $operationId = optional($operationAttribute)->id; 63 | $tags = $operationAttribute->tags ?? []; 64 | $servers = collect($operationAttribute->servers) 65 | ->filter(fn($server) => app($server) instanceof ServerFactory) 66 | ->map(static fn($server) => app($server)->build()) 67 | ->toArray(); 68 | 69 | $parameters = $this->parametersBuilder->build($route); 70 | $requestBody = $this->requestBodyBuilder->build($route); 71 | $responses = $this->responsesBuilder->build($route); 72 | $callbacks = $this->callbacksBuilder->build($route); 73 | $security = $this->securityBuilder->build($route); 74 | 75 | $operation = Operation::create() 76 | ->action(Str::lower($operationAttribute->method ?: $route->method)) 77 | ->tags(...$tags) 78 | ->deprecated($this->isDeprecated($route->actionDocBlock)) 79 | ->description($route->actionDocBlock?->getDescription()->render() ?: null) 80 | ->summary($route->actionDocBlock?->getSummary() ?: null) 81 | ->operationId($operationId) 82 | ->parameters(...$parameters) 83 | ->requestBody($requestBody) 84 | ->responses(...$responses) 85 | ->callbacks(...$callbacks) 86 | ->servers(...$servers); 87 | 88 | /** Not the cleanest code, we need to call notSecurity instead of security when our security has been turned off */ 89 | if (count($security) === 1 && $security[0]->securityScheme === null) { 90 | $operation = $operation->noSecurity(); 91 | } else { 92 | $operation = $operation->security(...$security); 93 | } 94 | 95 | $this->extensionsBuilder->build($operation, $route->actionAttributes); 96 | 97 | $operations[] = $operation; 98 | } 99 | 100 | return $operations; 101 | } 102 | 103 | protected function isDeprecated(?DocBlock $actionDocBlock): ?bool 104 | { 105 | if ($actionDocBlock === null) { 106 | return null; 107 | } 108 | 109 | $deprecatedTag = $actionDocBlock->getTagsByName('deprecated'); 110 | 111 | if (count($deprecatedTag) > 0) { 112 | return true; 113 | } 114 | 115 | return null; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Builders/PathsBuilder.php: -------------------------------------------------------------------------------- 1 | operationsBuilder = $operationsBuilder; 24 | } 25 | 26 | /** 27 | * @param string $collection 28 | * @param PathMiddleware[] $middlewares 29 | * @return array 30 | */ 31 | public function build( 32 | string $collection, 33 | array $middlewares 34 | ): array { 35 | return $this->routes() 36 | ->filter(static function (RouteInformation $routeInformation) use ($collection) { 37 | /** @var CollectionAttribute|null $collectionAttribute */ 38 | $collectionAttribute = collect() 39 | ->merge($routeInformation->controllerAttributes) 40 | ->merge($routeInformation->actionAttributes) 41 | ->first(static fn (object $item) => $item instanceof CollectionAttribute); 42 | 43 | return 44 | (! $collectionAttribute && $collection === Generator::COLLECTION_DEFAULT) || 45 | ($collectionAttribute && in_array($collection, $collectionAttribute->name, true)); 46 | }) 47 | ->map(static function (RouteInformation $item) use ($middlewares) { 48 | foreach ($middlewares as $middleware) { 49 | app($middleware)->before($item); 50 | } 51 | 52 | return $item; 53 | }) 54 | ->groupBy(static fn (RouteInformation $routeInformation) => $routeInformation->uri) 55 | ->map(function (Collection $routes, $uri) { 56 | $pathItem = PathItem::create()->route($uri); 57 | 58 | $operations = $this->operationsBuilder->build($routes); 59 | 60 | return $pathItem->operations(...$operations); 61 | }) 62 | ->map(static function (PathItem $item) use ($middlewares) { 63 | foreach ($middlewares as $middleware) { 64 | $item = app($middleware)->after($item); 65 | } 66 | 67 | return $item; 68 | }) 69 | ->values() 70 | ->toArray(); 71 | } 72 | 73 | protected function routes(): Collection 74 | { 75 | /** @noinspection CollectFunctionInCollectionInspection */ 76 | return collect(app(Router::class)->getRoutes()) 77 | ->filter(static fn (Route $route) => $route->getActionName() !== 'Closure') 78 | ->map(static fn (Route $route) => RouteInformation::createFromRoute($route)) 79 | ->filter(static function (RouteInformation $route) { 80 | $pathItem = $route->controllerAttributes 81 | ->first(static fn (object $attribute) => $attribute instanceof Attributes\PathItem); 82 | 83 | $operation = $route->actionAttributes 84 | ->first(static fn (object $attribute) => $attribute instanceof Attributes\Operation); 85 | 86 | return $pathItem && $operation; 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Builders/ServersBuilder.php: -------------------------------------------------------------------------------- 1 | map(static function (array $server) { 19 | $variables = collect(Arr::get($server, 'variables')) 20 | ->map(function (array $variable, string $key) { 21 | $serverVariable = ServerVariable::create($key) 22 | ->default(Arr::get($variable, 'default')) 23 | ->description(Arr::get($variable, 'description')); 24 | if (is_array(Arr::get($variable, 'enum'))) { 25 | return $serverVariable->enum(...Arr::get($variable, 'enum')); 26 | } 27 | 28 | return $serverVariable; 29 | }) 30 | ->toArray(); 31 | 32 | return Server::create() 33 | ->url(Arr::get($server, 'url')) 34 | ->description(Arr::get($server, 'description')) 35 | ->variables(...$variables); 36 | }) 37 | ->toArray(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Builders/TagsBuilder.php: -------------------------------------------------------------------------------- 1 | map(static function (array $tag) { 19 | $externalDocs = null; 20 | 21 | if (Arr::has($tag, 'externalDocs')) { 22 | $externalDocs = ExternalDocs::create($tag['name']) 23 | ->description(Arr::get($tag, 'externalDocs.description')) 24 | ->url(Arr::get($tag, 'externalDocs.url')); 25 | } 26 | 27 | return Tag::create() 28 | ->name($tag['name']) 29 | ->description(Arr::get($tag, 'description')) 30 | ->externalDocs($externalDocs); 31 | }) 32 | ->toArray(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ClassMapGenerator.php: -------------------------------------------------------------------------------- 1 | isFile()) { 27 | continue; 28 | } 29 | 30 | $path = $file->getRealPath() ?: $file->getPathname(); 31 | 32 | if ('php' !== pathinfo($path, PATHINFO_EXTENSION)) { 33 | continue; 34 | } 35 | 36 | $classes = self::findClasses($path); 37 | 38 | if (PHP_VERSION_ID >= 70000) { 39 | // PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098 40 | gc_mem_caches(); 41 | } 42 | 43 | foreach ($classes as $class) { 44 | $map[$class] = $path; 45 | } 46 | } 47 | 48 | return $map; 49 | } 50 | 51 | /** 52 | * Extract the classes in the given file. 53 | * 54 | * @param string $path The file to check 55 | * @return array The found classes 56 | */ 57 | private static function findClasses(string $path): array 58 | { 59 | $contents = file_get_contents($path); 60 | $tokens = token_get_all($contents); 61 | 62 | $nsTokens = [T_STRING => true, T_NS_SEPARATOR => true]; 63 | if (defined('T_NAME_QUALIFIED')) { 64 | $nsTokens[T_NAME_QUALIFIED] = true; 65 | } 66 | 67 | $classes = []; 68 | 69 | $namespace = ''; 70 | for ($i = 0; isset($tokens[$i]); $i++) { 71 | $token = $tokens[$i]; 72 | 73 | if (! isset($token[1])) { 74 | continue; 75 | } 76 | 77 | $class = ''; 78 | 79 | switch ($token[0]) { 80 | case T_NAMESPACE: 81 | $namespace = ''; 82 | // If there is a namespace, extract it 83 | while (isset($tokens[++$i][1])) { 84 | if (isset($nsTokens[$tokens[$i][0]])) { 85 | $namespace .= $tokens[$i][1]; 86 | } 87 | } 88 | $namespace .= '\\'; 89 | break; 90 | case T_CLASS: 91 | case T_INTERFACE: 92 | case T_TRAIT: 93 | // Skip usage of ::class constant 94 | $isClassConstant = false; 95 | for ($j = $i - 1; $j > 0; $j--) { 96 | if (! isset($tokens[$j][1])) { 97 | break; 98 | } 99 | 100 | if (T_DOUBLE_COLON === $tokens[$j][0]) { 101 | $isClassConstant = true; 102 | break; 103 | } 104 | 105 | if (! in_array($tokens[$j][0], [T_WHITESPACE, T_DOC_COMMENT, T_COMMENT], true)) { 106 | break; 107 | } 108 | } 109 | 110 | if ($isClassConstant) { 111 | break; 112 | } 113 | 114 | // Find the classname 115 | while (isset($tokens[++$i][1])) { 116 | $t = $tokens[$i]; 117 | if (T_STRING === $t[0]) { 118 | $class .= $t[1]; 119 | } elseif ('' !== $class && T_WHITESPACE === $t[0]) { 120 | break; 121 | } 122 | } 123 | 124 | $classes[] = ltrim($namespace.$class, '\\'); 125 | break; 126 | default: 127 | break; 128 | } 129 | } 130 | 131 | return $classes; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Concerns/Referencable.php: -------------------------------------------------------------------------------- 1 | build()->objectId, $objectId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/CallbackFactoryMakeCommand.php: -------------------------------------------------------------------------------- 1 | has($this->argument('collection')); 18 | 19 | if (! $collectionExists) { 20 | $this->error('Collection "'.$this->argument('collection').'" does not exist.'); 21 | 22 | return; 23 | } 24 | 25 | $this->line( 26 | $generator 27 | ->generate($this->argument('collection')) 28 | ->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/ParametersFactoryMakeCommand.php: -------------------------------------------------------------------------------- 1 | call('route:list'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Console/SchemaFactoryMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('model')) { 25 | return $this->buildModel($output, $model); 26 | } 27 | 28 | return $output; 29 | } 30 | 31 | protected function buildModel($output, $model) 32 | { 33 | $appVersion = explode('.', app()::VERSION); 34 | $namespace = $appVersion[0] >= 8 ? $this->laravel->getNamespace() . 'Models\\' : $this->laravel->getNamespace(); 35 | $model = Str::start($model, $namespace); 36 | 37 | if (!is_a($model, Model::class, true)) { 38 | throw new InvalidArgumentException('Invalid model'); 39 | } 40 | 41 | /** @var Model $model */ 42 | $model = app($model); 43 | 44 | $columns = SchemaFacade::getColumns($model->getTable()); 45 | 46 | $definition = 'return Schema::object(\'' . class_basename($model) . '\')' . PHP_EOL; 47 | $definition .= ' ->properties(' . PHP_EOL; 48 | 49 | $properties = collect($columns) 50 | ->map(static function (array $column) { 51 | $columnType = $column['type_name']; 52 | $default = $column['default'] ?? null; 53 | $notNull = ! $column['nullable']; 54 | $name = $column['name']; 55 | 56 | switch ($columnType) { 57 | case 'integer': 58 | case 'bigint': 59 | case 'smallint': 60 | $format = 'Schema::integer(%s)->default(%s)'; 61 | $args = [$name, $notNull ? (int)$default : null]; 62 | break; 63 | case 'boolean': 64 | $format = 'Schema::boolean(%s)->default(%s)'; 65 | $args = [$name, $notNull ? $default : null]; 66 | break; 67 | case 'date': 68 | case 'date_immutable': 69 | $format = 'Schema::string(%s)->format(Schema::FORMAT_DATE)->default(%s)'; 70 | $args = [$name, $notNull ? $default : null]; 71 | break; 72 | case 'datetime': 73 | case 'datetime_immutable': 74 | case 'datetimetz': 75 | case 'datetimetz_immutable': 76 | $format = 'Schema::string(%s)->format(Schema::FORMAT_DATE_TIME)->default(%s)'; 77 | $args = [$name, $notNull ? $default : null]; 78 | break; 79 | case 'decimal': 80 | case 'float': 81 | $format = 'Schema::number(%s)->format(Schema::FORMAT_FLOAT)->default(%s)'; 82 | $args = [$name, $notNull ? (float)$default : null]; 83 | break; 84 | case 'array': 85 | case 'json': 86 | $format = 'Schema::array(%s)->default(%s)'; 87 | $args = [$name, $notNull ? (array)$default : null]; 88 | break; 89 | case 'guid': 90 | case 'uuid': 91 | $format = 'Schema::string(%s)->format(Schema::FORMAT_UUID)->default(%s)'; 92 | $args = [$name, $notNull ? (array)$default : null]; 93 | break; 94 | case 'binary': 95 | $format = 'Schema::string(%s)->format(Schema::FORMAT_BINARY)->default(%s)'; 96 | $args = [$name, $notNull ? $default : null]; 97 | break; 98 | default: 99 | $format = 'Schema::string(%s)->default(%s)'; 100 | $args = [$name, $default]; 101 | break; 102 | } 103 | 104 | $args = array_map(static function ($value) { 105 | if ($value === null) { 106 | return 'null'; 107 | } 108 | 109 | if (is_numeric($value)) { 110 | return $value; 111 | } 112 | 113 | return '\'' . $value . '\''; 114 | }, $args); 115 | 116 | $indentation = str_repeat(' ', 4); 117 | 118 | return sprintf($indentation . $format, ...$args); 119 | }) 120 | ->implode(',' . PHP_EOL); 121 | 122 | $definition .= $properties . PHP_EOL; 123 | $definition .= ' );'; 124 | 125 | return str_replace('DummyDefinition', $definition, $output); 126 | } 127 | 128 | protected function getStub(): string 129 | { 130 | if ($this->option('model')) { 131 | return __DIR__ . '/stubs/schema.model.stub'; 132 | } 133 | 134 | return __DIR__ . '/stubs/schema.stub'; 135 | } 136 | 137 | protected function getDefaultNamespace($rootNamespace): string 138 | { 139 | return $rootNamespace . '\OpenApi\Schemas'; 140 | } 141 | 142 | protected function qualifyClass($name): string 143 | { 144 | $name = parent::qualifyClass($name); 145 | 146 | if (Str::endsWith($name, 'Schema')) { 147 | return $name; 148 | } 149 | 150 | return $name . 'Schema'; 151 | } 152 | 153 | protected function getOptions(): array 154 | { 155 | return [ 156 | ['model', 'm', InputOption::VALUE_OPTIONAL, 'The model class schema being generated for'], 157 | ['force', null, InputOption::VALUE_NONE, 'Create the class even if the factory already exists'], 158 | ]; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Console/SecuritySchemeFactoryMakeCommand.php: -------------------------------------------------------------------------------- 1 | route('{$request.body#/callbackUrl}') 19 | ->operations( 20 | Operation::post() 21 | ->requestBody( 22 | RequestBody::create() 23 | ->description('something happened') 24 | ->content( 25 | MediaType::json()->schema( 26 | Schema::object() 27 | ->properties( 28 | Schema::string('foo') 29 | ) 30 | ) 31 | ) 32 | ) 33 | ->responses( 34 | Response::ok()->description('Your server returns this code if it accepts the callback') 35 | ) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/stubs/extension.stub: -------------------------------------------------------------------------------- 1 | name('parameter-name') 20 | ->description('Parameter description') 21 | ->required(false) 22 | ->schema(Schema::string()), 23 | 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Console/stubs/requestbody.stub: -------------------------------------------------------------------------------- 1 | description('Successful response'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Console/stubs/schema.model.stub: -------------------------------------------------------------------------------- 1 | properties( 22 | Schema::string('foo') 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Console/stubs/securityscheme.stub: -------------------------------------------------------------------------------- 1 | type(SecurityScheme::TYPE_HTTP) 14 | ->scheme('bearer') 15 | ->bearerFormat('JWT'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Contracts/ComponentMiddleware.php: -------------------------------------------------------------------------------- 1 | config = $config; 35 | $this->infoBuilder = $infoBuilder; 36 | $this->serversBuilder = $serversBuilder; 37 | $this->tagsBuilder = $tagsBuilder; 38 | $this->pathsBuilder = $pathsBuilder; 39 | $this->componentsBuilder = $componentsBuilder; 40 | } 41 | 42 | public function generate(string $collection = self::COLLECTION_DEFAULT): OpenApi 43 | { 44 | $middlewares = Arr::get($this->config, 'collections.'.$collection.'.middlewares'); 45 | 46 | $info = $this->infoBuilder->build(Arr::get($this->config, 'collections.'.$collection.'.info', [])); 47 | $servers = $this->serversBuilder->build(Arr::get($this->config, 'collections.'.$collection.'.servers', [])); 48 | $tags = $this->tagsBuilder->build(Arr::get($this->config, 'collections.'.$collection.'.tags', [])); 49 | $paths = $this->pathsBuilder->build($collection, Arr::get($middlewares, 'paths', [])); 50 | $components = $this->componentsBuilder->build($collection, Arr::get($middlewares, 'components', [])); 51 | $extensions = Arr::get($this->config, 'collections.'.$collection.'.extensions', []); 52 | 53 | $openApi = OpenApi::create() 54 | ->openapi(OpenApi::OPENAPI_3_0_2) 55 | ->info($info) 56 | ->servers(...$servers) 57 | ->paths(...$paths) 58 | ->components($components) 59 | ->security(...Arr::get($this->config, 'collections.'.$collection.'.security', [])) 60 | ->tags(...$tags); 61 | 62 | foreach ($extensions as $key => $value) { 63 | $openApi = $openApi->x($key, $value); 64 | } 65 | 66 | return $openApi; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Http/OpenApiController.php: -------------------------------------------------------------------------------- 1 | generate(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/OpenApiServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 26 | __DIR__.'/../config/openapi.php', 27 | 'openapi' 28 | ); 29 | 30 | $this->app->bind(CallbacksBuilder::class, function () { 31 | return new CallbacksBuilder($this->getPathsFromConfig('callbacks')); 32 | }); 33 | 34 | $this->app->bind(RequestBodiesBuilder::class, function () { 35 | return new RequestBodiesBuilder($this->getPathsFromConfig('request_bodies')); 36 | }); 37 | 38 | $this->app->bind(ResponsesBuilder::class, function () { 39 | return new ResponsesBuilder($this->getPathsFromConfig('responses')); 40 | }); 41 | 42 | $this->app->bind(SchemasBuilder::class, function () { 43 | return new SchemasBuilder($this->getPathsFromConfig('schemas')); 44 | }); 45 | 46 | $this->app->bind(SecuritySchemesBuilder::class, function () { 47 | return new SecuritySchemesBuilder($this->getPathsFromConfig('security_schemes')); 48 | }); 49 | 50 | $this->app->singleton(Generator::class, static function (Application $app) { 51 | $config = config('openapi'); 52 | 53 | return new Generator( 54 | $config, 55 | $app->make(InfoBuilder::class), 56 | $app->make(ServersBuilder::class), 57 | $app->make(TagsBuilder::class), 58 | $app->make(PathsBuilder::class), 59 | $app->make(ComponentsBuilder::class) 60 | ); 61 | }); 62 | 63 | $this->commands([ 64 | Console\GenerateCommand::class, 65 | ]); 66 | 67 | if ($this->app->runningInConsole()) { 68 | $this->commands([ 69 | Console\CallbackFactoryMakeCommand::class, 70 | Console\ExtensionFactoryMakeCommand::class, 71 | Console\ParametersFactoryMakeCommand::class, 72 | Console\RequestBodyFactoryMakeCommand::class, 73 | Console\ResponseFactoryMakeCommand::class, 74 | Console\SchemaFactoryMakeCommand::class, 75 | Console\SecuritySchemeFactoryMakeCommand::class, 76 | ]); 77 | } 78 | } 79 | 80 | public function boot(): void 81 | { 82 | if ($this->app->runningInConsole()) { 83 | $this->publishes([ 84 | __DIR__.'/../config/openapi.php' => config_path('openapi.php'), 85 | ], 'openapi-config'); 86 | } 87 | 88 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 89 | } 90 | 91 | private function getPathsFromConfig(string $type): array 92 | { 93 | $directories = config('openapi.locations.'.$type, []); 94 | 95 | foreach ($directories as &$directory) { 96 | $directory = glob($directory, GLOB_ONLYDIR); 97 | } 98 | 99 | return (new Collection($directories)) 100 | ->flatten() 101 | ->unique() 102 | ->toArray(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/RouteInformation.php: -------------------------------------------------------------------------------- 1 | methods()) 51 | ->map(static fn ($value) => Str::lower($value)) 52 | ->filter(static fn ($value) => ! in_array($value, ['head', 'options'], true)) 53 | ->first(); 54 | 55 | $actionNameParts = explode('@', $route->getActionName()); 56 | 57 | if (count($actionNameParts) === 2) { 58 | [$controller, $action] = $actionNameParts; 59 | } else { 60 | [$controller] = $actionNameParts; 61 | $action = '__invoke'; 62 | } 63 | 64 | preg_match_all('/{(.*?)}/', $route->uri, $parameters); 65 | $parameters = collect($parameters[1]); 66 | 67 | if (count($parameters) > 0) { 68 | $parameters = $parameters->map(static fn ($parameter) => [ 69 | 'name' => Str::replaceLast('?', '', $parameter), 70 | 'required' => ! Str::endsWith($parameter, '?'), 71 | ]); 72 | } 73 | 74 | $reflectionClass = new ReflectionClass($controller); 75 | $reflectionMethod = $reflectionClass->getMethod($action); 76 | 77 | $docComment = $reflectionMethod->getDocComment(); 78 | $docBlock = $docComment ? DocBlockFactory::createInstance()->create($docComment) : null; 79 | 80 | $controllerAttributes = collect($reflectionClass->getAttributes()) 81 | ->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance()); 82 | 83 | $actionAttributes = collect($reflectionMethod->getAttributes()) 84 | ->map(fn (ReflectionAttribute $attribute) => $attribute->newInstance()); 85 | 86 | $containsControllerLevelParamter = $actionAttributes->contains(fn ($value) => $value instanceof \Vyuldashev\LaravelOpenApi\Attributes\Parameters); 87 | 88 | $instance->domain = $route->domain(); 89 | $instance->method = $method; 90 | $instance->uri = Str::start($route->uri(), '/'); 91 | $instance->name = $route->getName(); 92 | $instance->controller = $controller; 93 | $instance->parameters = $containsControllerLevelParamter ? collect([]) : $parameters; 94 | $instance->controllerAttributes = $controllerAttributes; 95 | $instance->action = $action; 96 | $instance->actionParameters = $reflectionMethod->getParameters(); 97 | $instance->actionAttributes = $actionAttributes; 98 | $instance->actionDocBlock = $docBlock; 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/SchemaHelpers.php: -------------------------------------------------------------------------------- 1 | getName()) { 13 | case 'int': 14 | return Schema::integer(); 15 | case 'bool': 16 | return Schema::boolean(); 17 | } 18 | 19 | return Schema::string(); 20 | } 21 | } 22 | --------------------------------------------------------------------------------