Update Database dan ALL yang di butuhkan

This commit is contained in:
Wian Drs
2026-06-19 14:35:42 +07:00
parent 5be2c5fbfe
commit 2f805a233d
9469 changed files with 1159473 additions and 25 deletions
@@ -0,0 +1,273 @@
---
name: write-tests
description: Write PHPUnit tests for the L5-Swagger Laravel package using Orchestra Testbench, following project conventions for mocking, config manipulation, and PHPUnit attributes
allowed-tools: Read, Edit, Write, Bash(vendor/bin/phpunit *), Bash(composer run-script phpunit), Bash(composer run-script analyse), Bash(grep *), Bash(find *), Bash(ls *), Bash(diff *), Bash(cat *)
---
# Write Tests for L5-Swagger
Generate PHPUnit tests for the L5-Swagger package following its established patterns.
## Context
- Test runner: !`composer run-script phpunit -- --version 2>/dev/null | head -1`
- Existing test files: !`ls tests/Unit/*.php 2>/dev/null | xargs -I{} basename {}`
- Source files: !`ls src/*.php src/**/*.php 2>/dev/null | grep -v vendor`
## Project Test Architecture
All tests extend `Tests\Unit\TestCase` which extends Orchestra Testbench's `OrchestraTestCase`. This gives every test a full Laravel application with the `L5SwaggerServiceProvider` registered.
### Base TestCase provides:
| Property / Method | Purpose |
|---|---|
| `$this->configFactory` | `ConfigFactory` instance resolved from the app container |
| `$this->generator` | `Generator` instance resolved from the app container |
| `$this->fileSystem` | PHPUnit mock of `Illuminate\Filesystem\Filesystem` (created via `createMock`) |
| `setAnnotationsPath()` | Points config to `tests/storage/annotations/OpenApi/` fixtures, enables `generate_always` and `generate_yaml_copy`, sets `L5_SWAGGER_CONST_HOST` constant, rebuilds the generator |
| `makeGeneratorWithMockedFileSystem()` | Injects `$this->fileSystem` mock into the generator via reflection |
| `setCustomDocsFileName($name, $type)` | Overrides docs filename in config for JSON or YAML |
| `crateJsonDocumentationFile()` | Creates a minimal `{}` JSON docs file |
| `createYamlDocumentationFile()` | Creates an empty YAML docs file |
| `jsonDocsFile()` | Returns absolute path to the JSON docs file (creates dir if needed) |
| `yamlDocsFile()` | Returns absolute path to the YAML docs file (creates dir if needed) |
| `copyAssets()` | Copies swagger-ui dist into testbench vendor dir (runs in setUp) |
| `deleteAssets()` | Removes the copied swagger-ui assets |
### tearDown cleanup
The base TestCase automatically deletes generated JSON/YAML docs files and the docs directory in `tearDown()`. You do NOT need to clean up generated files.
## Instructions
### Step 1: Determine what to test
Read the source file(s) the user wants tested. Identify:
- Public methods and their behavior
- Error/exception paths
- Config-driven behavior branches
- Interactions with the filesystem or external dependencies
### Step 2: Create or edit the test file
**File location**: `tests/Unit/{ClassName}Test.php`
**Required class-level attributes** (PHPUnit 11 attributes, not annotations):
```php
#[TestDox('Human readable class description')]
#[CoversClass(FullyQualifiedClassName::class)]
```
**Required test method conventions**:
- Method names: `testItDoesX` or `testCanDoX` (camelCase, descriptive)
- Visibility: `public function testXxx(): void`
- Add `@throws` docblock for expected exceptions
- Use `expectException()` and `expectExceptionMessage()` for exception tests
### Step 3: Follow these patterns based on what you're testing
#### Pattern A: Testing Generator behavior with mocked filesystem
Use when testing `Generator` methods that interact with the filesystem (directory creation, file writing, permission checks).
```php
public function testItThrowsExceptionIfSomethingFails(): void
{
$this->setAnnotationsPath();
$config = $this->configFactory->documentationConfig();
$docs = $config['paths']['docs'];
// Set up filesystem mock expectations
$this->fileSystem
->expects($this->once())
->method('exists')
->with($docs)
->willReturn(true);
// ... more mock setup ...
$this->expectException(L5SwaggerException::class);
$this->expectExceptionMessage('Expected error message');
// IMPORTANT: call makeGeneratorWithMockedFileSystem() AFTER mock setup
$this->makeGeneratorWithMockedFileSystem();
$this->generator->generateDocs();
}
```
#### Pattern B: Testing full generation pipeline (integration)
Use when testing that docs generate correctly with specific config.
```php
public function testCanGenerateWithSpecificConfig(): void
{
$this->setAnnotationsPath();
// Optionally override config
$cfg = config('l5-swagger.documentations.default');
$cfg['paths']['base'] = 'https://custom-server.url';
config(['l5-swagger' => [
'default' => 'default',
'documentations' => ['default' => $cfg],
'defaults' => config('l5-swagger.defaults'),
]]);
$this->generator->generateDocs();
$this->assertFileExists($this->jsonDocsFile());
// Verify via HTTP response
$this->get(route('l5-swagger.default.docs'))
->assertSee('expected content')
->assertStatus(200);
}
```
#### Pattern C: Testing routes and HTTP responses
Use when testing controller behavior, middleware, or route registration.
```php
public function testRouteReturnsExpectedResponse(): void
{
// For tests needing generated docs, call setAnnotationsPath() first
// For tests checking behavior without docs, don't call it
$this->get(route('l5-swagger.default.docs'))
->assertStatus(200)
->assertSee('expected')
->assertHeader('Content-Type', 'application/json');
}
```
#### Pattern D: Testing config merging
Use when testing `ConfigFactory` behavior.
```php
#[DataProvider('configDataProvider')]
public function testConfigMergesCorrectly(array $data, array $expected): void
{
config(['l5-swagger' => array_merge($data, [
'defaults' => [/* base defaults */],
])]);
$config = $this->configFactory->documentationConfig();
$this->assertSame($expected['key'], $config['key']);
}
public static function configDataProvider(): \Generator
{
yield 'descriptive case name' => [
'data' => [/* input */],
'expected' => [/* expected output */],
];
}
```
#### Pattern E: Mocking the Generator via GeneratorFactory
Use when testing controllers/routes that call `generateDocs()` and you want to control generator behavior without actual generation.
```php
public function testBehaviorWhenGenerationFails(): void
{
$mockGenerator = $this->createMock(Generator::class);
$mockGeneratorFactory = $this->createMock(GeneratorFactory::class);
$mockGeneratorFactory->method('make')->willReturn($mockGenerator);
app()->extend(GeneratorFactory::class, function () use ($mockGeneratorFactory) {
return $mockGeneratorFactory;
});
$mockGenerator->expects($this->once())
->method('generateDocs')
->willThrowException(new L5SwaggerException());
$this->get(route('l5-swagger.default.docs'))->assertNotFound();
}
```
### Step 4: Config manipulation pattern
When overriding config, always preserve the full structure:
```php
config(['l5-swagger' => [
'default' => 'default',
'documentations' => [
'default' => $cfg, // your modified config
],
'defaults' => config('l5-swagger.defaults'),
]]);
```
After changing config that affects the generator, call `$this->makeGenerator()` to rebuild it.
### Step 5: Run and verify with coverage gate
New tests must never decrease code coverage. Follow this sequence:
#### 5a. Capture baseline coverage BEFORE writing tests
```bash
vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-before.txt
```
Extract the baseline percentages:
```bash
grep -E 'Lines:|Methods:|Classes:' /tmp/l5-coverage-before.txt
```
Record both **Lines** and **Methods** percentages — these are the numbers that must not drop.
#### 5b. Run the new/modified tests in isolation
```bash
vendor/bin/phpunit tests/Unit/YourNewTest.php --testdox
```
#### 5c. Run the full suite with coverage AFTER adding tests
```bash
vendor/bin/phpunit --coverage-text --only-summary-for-coverage-text 2>&1 | tee /tmp/l5-coverage-after.txt
```
#### 5d. Compare coverage
```bash
diff /tmp/l5-coverage-before.txt /tmp/l5-coverage-after.txt
```
Verify:
- **Lines coverage**: must be >= baseline
- **Methods coverage**: must be >= baseline
If coverage decreased, identify the cause:
1. A new test file with `#[CoversClass]` pulled in a class that was previously uncovered — add tests for the uncovered methods
2. A test is covering code paths that were already covered but skipping others — add assertions for the missing branches
3. A new fixture or helper class landed under `src/` — it needs its own tests
**Do not proceed until coverage is equal to or higher than the baseline.**
#### 5e. Run static analysis
```bash
composer run-script analyse
```
## Important Rules
- **Coverage must not decrease.** Always capture baseline coverage before writing tests and verify it afterwards. If a `#[CoversClass]` attribute pulls in a class with uncovered methods, you must add tests for those methods too — not just remove the attribute. This is a hard gate: do not report the task as complete until coverage is verified equal or higher.
- Use PHP 8.2+ features: constructor property promotion, union types, named arguments, match expressions
- Use PHPUnit 11 **attributes** (`#[TestDox]`, `#[CoversClass]`, `#[DataProvider]`), NOT docblock annotations (`@testdox`, `@covers`, `@dataProvider`)
- Data providers must be `public static` methods returning `\Generator` (using `yield`)
- Fixture annotations live in `tests/storage/annotations/OpenApi/` — read existing ones before creating new fixtures
- The `$this->fileSystem` mock is created fresh via `#[Before]` attribute before each test — no shared mock state between tests
- Route names follow the pattern `l5-swagger.{documentation}.{type}` where type is `api`, `docs`, `asset`, or `oauth2_callback`
- The test environment sets `SWAGGER_VERSION=3.0` and `APP_KEY` via `phpunit.xml`
- StyleCI enforces Laravel preset — don't fight the code style
+83
View File
@@ -0,0 +1,83 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
L5-Swagger is a Laravel package that integrates swagger-php and swagger-ui into Laravel. It does NOT implement the OpenAPI spec — it wraps [swagger-php](https://github.com/zircote/swagger-php) for annotation scanning and [swagger-ui](https://github.com/swagger-api/swagger-ui) for the documentation frontend.
Supports multiple independent documentation sets, each with its own routes, paths, and settings via a two-level config: `defaults` (shared) merged with `documentations.{name}` (per-doc overrides). `ConfigFactory::mergeConfig()` recursively merges associative arrays.
## Development Commands
```bash
# Run all tests
composer run-script phpunit
# Run a single test file
vendor/bin/phpunit tests/Unit/GeneratorTest.php
# Run a single test method
vendor/bin/phpunit --filter testCanGenerateApiJsonFile
# Static analysis (PHPStan level 8)
composer run-script analyse
```
## Architecture
### Generation Pipeline
`GeneratorFactory::make(documentation)` → creates `Generator` with merged config → `Generator::generateDocs()` chains:
1. `prepareDirectory()` — ensure output dir exists/writable
2. `defineConstants()` — define PHP constants from config (usable in annotations)
3. `scanFilesForDocumentation()` — swagger-php scans PHP files using `ReflectionAnalyser` + `AttributeAnnotationFactory`
4. `populateServers()` — injects base path as a Server URL
5. `saveJson()` — writes JSON, then `SecurityDefinitions::generate()` injects security schemes
6. `makeYamlCopy()` — optional YAML conversion via `symfony/yaml`
Custom processors can be positioned relative to any existing processor via `scanOptions.processors` using `['class' => ..., 'after' => ...]` syntax. A `CustomGeneratorInterface` implementation can be provided via `scanOptions.generator_factory` to supply a pre-configured `OpenApi\Generator`.
### Service Container Bindings
`L5SwaggerServiceProvider` binds `Generator::class` as a factory — each resolution creates a new Generator via `GeneratorFactory::make()` using the `l5-swagger.default` documentation name. The `GenerateDocsCommand` is registered as a singleton under `command.l5-swagger.generate`.
### Routing
Routes are registered dynamically in `src/routes.php` by iterating all documentation sets. Each set gets up to 4 routes (api UI, docs JSON/YAML, assets, OAuth2 callback), all wrapped with the `Config` middleware that sets the active documentation context. Route names follow `l5-swagger.{documentation}.{type}`.
### Helpers
`swagger_ui_dist_path()` resolves swagger-ui asset paths with an allowlist of permitted files. `l5_swagger_asset()` generates versioned (cache-busted) URLs for those assets.
## Testing
Tests use Orchestra Testbench (`Tests\Unit\TestCase` extends `OrchestraTestCase`) for a full Laravel environment.
Key patterns:
- `$this->fileSystem` is a PHPUnit mock of `Filesystem`, injected into `Generator` via reflection in `makeGeneratorWithMockedFileSystem()`
- `setAnnotationsPath()` reconfigures the app to use `tests/storage/annotations/OpenApi/` fixtures and rebuilds the generator
- `setCustomDocsFileName()` overrides the output filename for JSON/YAML format tests
- `copyAssets()` copies swagger-ui dist into testbench's vendor directory in setUp; `tearDown` cleans up generated docs
- PHPUnit attributes: `#[TestDox('...')]`, `#[CoversClass(...)]`
Test fixtures live in `tests/storage/annotations/OpenApi/` with sample PHP attribute annotations.
## Code Style
- **StyleCI**: `preset: laravel` — enforced automatically
- **PHPStan**: Level 8, analyzes `src/` and `tests/Unit/`, ignores `argument.templateType`
- PHP 8.2+ required; uses constructor property promotion, union types, named arguments
- Supports Laravel 11.44+, 12.1+, and 13.0+
## Environment Variables
| Variable | Purpose |
|---|---|
| `L5_SWAGGER_GENERATE_ALWAYS` | Regenerate docs on every request (dev mode) |
| `L5_SWAGGER_GENERATE_YAML_COPY` | Also create YAML version |
| `L5_SWAGGER_CONST_HOST` | Default host constant for annotations |
| `L5_SWAGGER_OPEN_API_SPEC_VERSION` | `3.0.0` (default) or `3.1.0` |
| `L5_SWAGGER_UI_DARK_MODE` | Dark mode for Swagger UI |
| `SWAGGER_VERSION` | Used in tests to set OpenAPI version |
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Darius Matulionis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+65
View File
@@ -0,0 +1,65 @@
{
"name": "darkaonline/l5-swagger",
"description": "OpenApi or Swagger integration to Laravel",
"keywords": [
"laravel",
"swagger",
"api",
"OpenApi",
"specification",
"documentation",
"API",
"UI"
],
"license": "MIT",
"authors": [
{
"name": "Darius Matulionis",
"email": "darius@matulionis.lt"
}
],
"autoload": {
"psr-4": {
"L5Swagger\\": "src"
},
"files": [
"src/helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests"
}
},
"require": {
"php": "^8.2",
"laravel/framework": "^13.0 || ^12.1 || ^11.44",
"zircote/swagger-php": "^6.0",
"swagger-api/swagger-ui": ">=5.18.3",
"symfony/yaml": "^5.0 || ^6.0 || ^7.0 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"mockery/mockery": "1.*",
"orchestra/testbench": "^11.0 || ^10.0 || ^9.0 || ^8.0 || 7.* || ^6.15 || 5.*",
"php-coveralls/php-coveralls": "^2.0",
"phpstan/phpstan": "^2.1"
},
"extra": {
"laravel": {
"providers": [
"L5Swagger\\L5SwaggerServiceProvider"
],
"aliases": {
"L5Swagger": "L5Swagger\\L5SwaggerFacade"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"phpunit": "vendor/bin/phpunit --testdox",
"analyse": "vendor/bin/phpstan analyse --memory-limit=256M"
}
}
+333
View File
@@ -0,0 +1,333 @@
<?php
return [
'default' => 'default',
'documentations' => [
'default' => [
'api' => [
'title' => 'L5 Swagger UI',
],
'routes' => [
/*
* Route for accessing api documentation interface
*/
'api' => 'api/documentation',
],
'paths' => [
/*
* Edit to include full URL in ui for assets
*/
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
/*
* Edit to set path where swagger ui assets should be stored
*/
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
/*
* File name of the generated json documentation file
*/
'docs_json' => 'api-docs.json',
/*
* File name of the generated YAML documentation file
*/
'docs_yaml' => 'api-docs.yaml',
/*
* Set this to `json` or `yaml` to determine which documentation file to use in UI
*/
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
/*
* Absolute paths to directory containing the swagger annotations are stored.
*/
'annotations' => [
base_path('app'),
],
],
],
],
'defaults' => [
'routes' => [
/*
* Route for accessing parsed swagger annotations.
*/
'docs' => 'docs',
/*
* Route for Oauth2 authentication callback.
*/
'oauth2_callback' => 'api/oauth2-callback',
/*
* Middleware allows to prevent unexpected access to API documentation
*/
'middleware' => [
'api' => [],
'asset' => [],
'docs' => [],
'oauth2_callback' => [],
],
/*
* Route Group options
*/
'group_options' => [],
],
'paths' => [
/*
* Absolute path to location where parsed annotations will be stored
*/
'docs' => storage_path('api-docs'),
/*
* Absolute path to directory where to export views
*/
'views' => base_path('resources/views/vendor/l5-swagger'),
/*
* Edit to set the api's base path
*/
'base' => env('L5_SWAGGER_BASE_PATH', null),
/*
* Absolute path to directories that should be excluded from scanning
* @deprecated Please use `scanOptions.exclude`
* `scanOptions.exclude` overwrites this
*/
'excludes' => [],
],
'scanOptions' => [
/**
* Optional CustomGeneratorInterface implementation that creates an OpenApi\Generator instance.
* Use this to provide a custom pre-configured generator.
* Accepts an instance or a class name (FQCN) implementing the interface.
*
* @see \L5Swagger\CustomGeneratorInterface
*/
'generator_factory' => null,
/**
* Configuration for default processors. Allows to pass processors configuration to swagger-php.
*
* @link https://zircote.github.io/swagger-php/reference/processors.html
*/
'default_processors_configuration' => [
/** Example */
/**
* 'operationId.hash' => true,
* 'pathFilter' => [
* 'tags' => [
* '/pets/',
* '/store/',
* ],
* ],.
*/
],
/**
* analyser: defaults to \OpenApi\StaticAnalyser .
*
* @see \OpenApi\scan
*/
'analyser' => null,
/**
* analysis: defaults to a new \OpenApi\Analysis .
*
* @see \OpenApi\scan
*/
'analysis' => null,
/**
* Custom processors.
*
* Each entry can be:
* - A class name or instance (inserted after BuildPaths by default)
* - An array with 'class' and 'after' keys for precise positioning:
* ['class' => MyProcessor::class, 'after' => SomeProcessor::class]
*
* @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter
* @see \OpenApi\scan
*/
'processors' => [
// \App\SwaggerProcessors\SchemaQueryParameter::class,
// ['class' => \App\SwaggerProcessors\Custom::class, 'after' => \OpenApi\Processors\AugmentSchemas::class],
],
/**
* pattern: string $pattern File pattern(s) to scan (default: *.php) .
*
* @see \OpenApi\scan
*/
'pattern' => null,
/*
* Absolute path to directories that should be excluded from scanning
* @note This option overwrites `paths.excludes`
* @see \OpenApi\scan
*/
'exclude' => [],
/*
* Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0.
* By default the spec will be in version 3.0.0
*/
'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION),
],
/*
* API security definitions. Will be generated into documentation file.
*/
'securityDefinitions' => [
'securitySchemes' => [
/*
* Examples of Security schemes
*/
/*
'api_key_security_example' => [ // Unique name of security
'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for security scheme',
'name' => 'api_key', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
'oauth2_security_example' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for oauth2 security scheme.',
'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode".
'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode)
//'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode)
'scopes' => [
'read:projects' => 'read your projects',
'write:projects' => 'modify projects in your account',
]
],
*/
/* Open API 3.0 support
'passport' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Laravel passport oauth2 security.',
'in' => 'header',
'scheme' => 'https',
'flows' => [
"password" => [
"authorizationUrl" => config('app.url') . '/oauth/authorize',
"tokenUrl" => config('app.url') . '/oauth/token',
"refreshUrl" => config('app.url') . '/token/refresh',
"scopes" => []
],
],
],
'sanctum' => [ // Unique name of security
'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Enter token in format (Bearer <token>)',
'name' => 'Authorization', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
*/
],
'security' => [
/*
* Examples of Securities
*/
[
/*
'oauth2_security_example' => [
'read',
'write'
],
'passport' => []
*/
],
],
],
/*
* Set this to `true` in development mode so that docs would be regenerated on each request
* Set this to `false` to disable swagger generation on production
*/
'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
/*
* Set this to `true` to generate a copy of documentation in yaml format
*/
'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false),
/*
* Edit to trust the proxy's ip address - needed for AWS Load Balancer
* string[]
*/
'proxy' => false,
/*
* Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle.
* See more at: https://github.com/swagger-api/swagger-ui#configs-plugin
*/
'additional_config_url' => null,
/*
* Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically),
* 'method' (sort by HTTP method).
* Default is the order returned by the server unchanged.
*/
'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null),
/*
* Pass the validatorUrl parameter to SwaggerUi init on the JS side.
* A null value here disables validation.
*/
'validator_url' => null,
/*
* Swagger UI configuration parameters
*/
'ui' => [
'display' => [
'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false),
/*
* Controls the default expansion setting for the operations and tags. It can be :
* 'list' (expands only the tags),
* 'full' (expands the tags and operations),
* 'none' (expands nothing).
*/
'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'),
/**
* If set, enables filtering. The top bar will show an edit box that
* you can use to filter the tagged operations that are shown. Can be
* Boolean to enable or disable, or a string, in which case filtering
* will be enabled using that string as the filter expression. Filtering
* is case-sensitive matching the filter expression anywhere inside
* the tag.
*/
'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false
],
'authorization' => [
/*
* If set to true, it persists authorization data, and it would not be lost on browser close/refresh
*/
'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false),
'oauth2' => [
/*
* If set to true, adds PKCE to AuthorizationCodeGrant flow
*/
'use_pkce_with_authorization_code_grant' => false,
],
],
],
/*
* Constants which can be used in annotations
*/
'constants' => [
'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),
],
],
];
+7
View File
@@ -0,0 +1,7 @@
parameters:
level: 8
paths:
- src
- tests/Unit
ignoreErrors:
- identifier: argument.templateType
@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ $documentationTitle }}</title>
<link rel="stylesheet" type="text/css" href="{{ l5_swagger_asset($documentation, 'swagger-ui.css') }}">
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-32x32.png') }}" sizes="32x32"/>
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-16x16.png') }}" sizes="16x16"/>
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
@if(config('l5-swagger.defaults.ui.display.dark_mode'))
<style>
body#dark-mode,
#dark-mode .scheme-container {
background: #1b1b1b;
}
#dark-mode .scheme-container,
#dark-mode .opblock .opblock-section-header{
box-shadow: 0 1px 2px 0 rgba(255, 255, 255, 0.15);
}
#dark-mode .operation-filter-input,
#dark-mode .dialog-ux .modal-ux,
#dark-mode input[type=email],
#dark-mode input[type=file],
#dark-mode input[type=password],
#dark-mode input[type=search],
#dark-mode input[type=text],
#dark-mode textarea{
background: #343434;
color: #e7e7e7;
}
#dark-mode .title,
#dark-mode li,
#dark-mode p,
#dark-mode table,
#dark-mode label,
#dark-mode .opblock-tag,
#dark-mode .opblock .opblock-summary-operation-id,
#dark-mode .opblock .opblock-summary-path,
#dark-mode .opblock .opblock-summary-path__deprecated,
#dark-mode h1,
#dark-mode h2,
#dark-mode h3,
#dark-mode h4,
#dark-mode h5,
#dark-mode .btn,
#dark-mode .tab li,
#dark-mode .parameter__name,
#dark-mode .parameter__type,
#dark-mode .prop-format,
#dark-mode .loading-container .loading:after{
color: #e7e7e7;
}
#dark-mode .opblock-description-wrapper p,
#dark-mode .opblock-external-docs-wrapper p,
#dark-mode .opblock-title_normal p,
#dark-mode .response-col_status,
#dark-mode table thead tr td,
#dark-mode table thead tr th,
#dark-mode .response-col_links,
#dark-mode .swagger-ui{
color: wheat;
}
#dark-mode .parameter__extension,
#dark-mode .parameter__in,
#dark-mode .model-title{
color: #949494;
}
#dark-mode table thead tr td,
#dark-mode table thead tr th{
border-color: rgba(120,120,120,.2);
}
#dark-mode .opblock .opblock-section-header{
background: transparent;
}
#dark-mode .opblock.opblock-post{
background: rgba(73,204,144,.25);
}
#dark-mode .opblock.opblock-get{
background: rgba(97,175,254,.25);
}
#dark-mode .opblock.opblock-put{
background: rgba(252,161,48,.25);
}
#dark-mode .opblock.opblock-delete{
background: rgba(249,62,62,.25);
}
#dark-mode .loading-container .loading:before{
border-color: rgba(255,255,255,10%);
border-top-color: rgba(255,255,255,.6);
}
#dark-mode svg:not(:root){
fill: #e7e7e7;
}
#dark-mode .opblock-summary-description {
color: #fafafa;
}
</style>
@endif
</head>
<body @if(config('l5-swagger.defaults.ui.display.dark_mode')) id="dark-mode" @endif>
<div id="swagger-ui"></div>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-bundle.js') }}"></script>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-standalone-preset.js') }}"></script>
<script>
window.onload = function() {
const urls = [];
@foreach($urlsToDocs as $title => $url)
urls.push({name: "{{ $title }}", url: "{{ $url }}"});
@endforeach
// Build a system
const ui = SwaggerUIBundle({
dom_id: '#swagger-ui',
urls: urls,
"urls.primaryName": "{{ $documentationTitle }}",
operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!},
configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!},
validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!},
oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}",
requestInterceptor: function(request) {
request.headers['X-CSRF-TOKEN'] = '{{ csrf_token() }}';
return request;
},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}",
deepLinking: true,
filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!},
persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}",
})
window.ui = ui
@if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type')))
ui.initOAuth({
usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}"
})
@endif
}
</script>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace L5Swagger;
use L5Swagger\Exceptions\L5SwaggerException;
class ConfigFactory
{
/**
* Retrieves and merges the configuration for the specified documentation.
*
* @param string|null $documentation The name of the documentation configuration to retrieve.
* If null, the default documentation configuration is used.
* @return array<string, mixed> The merged configuration for the specified documentation.
*
* @throws L5SwaggerException If the specified documentation configuration is not found.
*/
public function documentationConfig(?string $documentation = null): array
{
if ($documentation === null) {
$documentation = config('l5-swagger.default');
}
$defaults = config('l5-swagger.defaults', []);
$documentations = config('l5-swagger.documentations', []);
if (! isset($documentations[$documentation])) {
throw new L5SwaggerException('Documentation config not found');
}
return $this->mergeConfig($defaults, $documentations[$documentation]);
}
/**
* Merges two configuration arrays recursively, with the values from the second array
* overriding those in the first array when keys overlap.
*
* @param array<string, mixed> $defaults The default configuration array.
* @param array<string, mixed> $config The configuration array to merge into the defaults.
* @return array<string, mixed> The merged configuration array.
*/
private function mergeConfig(array $defaults, array $config): array
{
$merged = $defaults;
foreach ($config as $key => &$value) {
if (isset($defaults[$key])
&& $this->isAssociativeArray($defaults[$key])
&& $this->isAssociativeArray($value)
) {
$merged[$key] = $this->mergeConfig($defaults[$key], $value);
continue;
}
$merged[$key] = $value;
}
return $merged;
}
/**
* Determines whether a given value is an associative array.
*
* @param mixed $value The value to be checked.
* @return bool True if the value is an associative array, false otherwise.
*/
private function isAssociativeArray(mixed $value): bool
{
return is_array($value) && count(array_filter(array_keys($value), 'is_string')) > 0;
}
}
@@ -0,0 +1,75 @@
<?php
namespace L5Swagger\Console;
use Illuminate\Console\Command;
use L5Swagger\Exceptions\L5SwaggerException;
use L5Swagger\GeneratorFactory;
class GenerateDocsCommand extends Command
{
public function __construct()
{
parent::__construct();
}
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'l5-swagger:generate {documentation?} {--all}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Regenerate docs';
/**
* @param GeneratorFactory $generatorFactory
*
* @throws L5SwaggerException
*/
public function handle(GeneratorFactory $generatorFactory): void
{
$all = $this->option('all');
if ($all) {
/** @var array<string> $documentations */
$documentations = array_keys(config('l5-swagger.documentations', []));
foreach ($documentations as $documentation) {
$this->generateDocumentation($generatorFactory, $documentation);
}
return;
}
$documentation = $this->argument('documentation');
if (! $documentation) {
$documentation = config('l5-swagger.default');
}
$this->generateDocumentation($generatorFactory, $documentation);
}
/**
* Generates documentation using the specified generator factory.
*
* @param GeneratorFactory $generatorFactory The factory used to create the documentation generator.
* @param string $documentation The name or identifier of the documentation to be generated.
* @return void
*
* @throws L5SwaggerException
*/
private function generateDocumentation(GeneratorFactory $generatorFactory, string $documentation): void
{
$this->info('Regenerating docs '.$documentation);
$generator = $generatorFactory->make($documentation);
$generator->generateDocs();
}
}
@@ -0,0 +1,10 @@
<?php
namespace L5Swagger;
use OpenApi\Generator as OpenApiGenerator;
interface CustomGeneratorInterface
{
public function create(): OpenApiGenerator;
}
@@ -0,0 +1,7 @@
<?php
namespace L5Swagger\Exceptions;
class L5SwaggerException extends \Exception
{
}
+359
View File
@@ -0,0 +1,359 @@
<?php
namespace L5Swagger;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use L5Swagger\Exceptions\L5SwaggerException;
use OpenApi\Annotations\OpenApi;
use OpenApi\Annotations\Server;
use OpenApi\Generator as OpenApiGenerator;
use OpenApi\OpenApiException;
use OpenApi\Pipeline;
use OpenApi\SourceFinder;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Yaml\Dumper as YamlDumper;
use Symfony\Component\Yaml\Yaml;
class Generator
{
public const OPEN_API_DEFAULT_SPEC_VERSION = '3.0.0';
protected const SCAN_OPTION_PROCESSORS = 'processors';
protected const SCAN_OPTION_PATTERN = 'pattern';
protected const SCAN_OPTION_ANALYSER = 'analyser';
protected const SCAN_OPTION_ANALYSIS = 'analysis';
protected const SCAN_OPTION_EXCLUDE = 'exclude';
protected const AVAILABLE_SCAN_OPTIONS = [
self::SCAN_OPTION_PATTERN,
self::SCAN_OPTION_ANALYSER,
self::SCAN_OPTION_ANALYSIS,
self::SCAN_OPTION_EXCLUDE,
];
/**
* @var string|array<string>
*/
protected string|array $annotationsDir;
protected string $docDir;
protected string $docsFile;
protected string $yamlDocsFile;
/**
* @var array<string>
*/
protected array $excludedDirs;
/**
* @var array<string>
*/
protected array $constants;
protected ?OpenApi $openApi;
protected bool $yamlCopyRequired;
protected ?string $basePath;
protected SecurityDefinitions $security;
/**
* @var array<string,mixed>
*/
protected array $scanOptions;
protected Filesystem $fileSystem;
/**
* Constructor to initialize documentation generation settings and dependencies.
*
* @param array<string,mixed> $paths Array of paths including annotations, docs, excluded directories, and base path.
* @param array<string> $constants Array of constants to be used during documentation generation.
* @param bool $yamlCopyRequired Determines if a YAML copy of the documentation is required.
* @param SecurityDefinitions $security Security definitions for the documentation.
* @param array<string> $scanOptions Additional options for scanning files or directories.
* @param Filesystem|null $filesystem Filesystem instance, optional, defaults to a new Filesystem.
* @return void
*/
public function __construct(
array $paths,
array $constants,
bool $yamlCopyRequired,
SecurityDefinitions $security,
array $scanOptions,
?Filesystem $filesystem = null
) {
$this->annotationsDir = $paths['annotations'];
$this->docDir = $paths['docs'];
$this->docsFile = $this->docDir.DIRECTORY_SEPARATOR.($paths['docs_json'] ?? 'api-docs.json');
$this->yamlDocsFile = $this->docDir.DIRECTORY_SEPARATOR.($paths['docs_yaml'] ?? 'api-docs.yaml');
$this->excludedDirs = $paths['excludes'];
$this->basePath = $paths['base'];
$this->constants = $constants;
$this->yamlCopyRequired = $yamlCopyRequired;
$this->security = $security;
$this->scanOptions = $scanOptions;
$this->fileSystem = $filesystem ?? new Filesystem();
}
/**
* Generate necessary documentation files by scanning and processing the required data.
*
* @return void
*
* @throws L5SwaggerException
* @throws Exception
*/
public function generateDocs(): void
{
$this->prepareDirectory()
->defineConstants()
->scanFilesForDocumentation()
->populateServers()
->saveJson()
->makeYamlCopy();
}
/**
* Prepares the directory for storing documentation by ensuring it exists and is writable.
*
* @return self
*
* @throws L5SwaggerException If the directory is not writable or cannot be created.
*/
protected function prepareDirectory(): self
{
if ($this->fileSystem->exists($this->docDir) && ! $this->fileSystem->isWritable($this->docDir)) {
throw new L5SwaggerException('Documentation storage directory is not writable');
}
if (! $this->fileSystem->exists($this->docDir)) {
$this->fileSystem->makeDirectory($this->docDir);
}
if (! $this->fileSystem->exists($this->docDir)) {
throw new L5SwaggerException('Documentation storage directory could not be created');
}
return $this;
}
/**
* Define and set constants if not already defined.
*
* @return self
*/
protected function defineConstants(): self
{
if (! empty($this->constants)) {
foreach ($this->constants as $key => $value) {
defined($key) || define($key, $value);
}
}
return $this;
}
/**
* Scans files to generate documentation.
*
* @return self
*/
protected function scanFilesForDocumentation(): self
{
$generator = $this->createOpenApiGenerator();
$finder = $this->createScanFinder();
// Analysis.
$analysis = Arr::get($this->scanOptions, self::SCAN_OPTION_ANALYSIS);
$this->openApi = $generator->generate($finder, $analysis);
return $this;
}
/**
* Create and configure an instance of OpenApiGenerator.
*
* @return OpenApiGenerator
*/
protected function createOpenApiGenerator(): OpenApiGenerator
{
$factory = $this->scanOptions['generator_factory'] ?? null;
if (is_string($factory) && is_subclass_of($factory, CustomGeneratorInterface::class)) {
$factory = new $factory();
}
$generator = $factory instanceof CustomGeneratorInterface
? $factory->create()
: new OpenApiGenerator();
if (! empty($this->scanOptions['default_processors_configuration'])
&& is_array($this->scanOptions['default_processors_configuration'])
) {
$generator->setConfig($this->scanOptions['default_processors_configuration']);
}
$generator->setVersion(
$this->scanOptions['open_api_spec_version'] ?? self::OPEN_API_DEFAULT_SPEC_VERSION
);
// Processors.
$this->setProcessors($generator);
// Analyser.
$this->setAnalyser($generator);
return $generator;
}
/**
* Set the processors for the OpenAPI generator.
*
* @param OpenApiGenerator $generator The OpenAPI generator instance to configure.
* @return void
*/
protected function setProcessors(OpenApiGenerator $generator): void
{
$processorConfigs = Arr::get($this->scanOptions, self::SCAN_OPTION_PROCESSORS, []);
if (empty($processorConfigs)) {
return;
}
$normalizedConfigs = [];
foreach ($processorConfigs as $config) {
$normalizedConfigs[] = is_array($config)
? $config
: ['class' => $config, 'after' => \OpenApi\Processors\BuildPaths::class];
}
$newPipeLine = [];
$generator->getProcessorPipeline()->walk(
function (callable $pipe) use ($normalizedConfigs, &$newPipeLine) {
$newPipeLine[] = $pipe;
foreach ($normalizedConfigs as $entry) {
$after = $entry['after'];
if ($pipe instanceof $after) {
$processor = $entry['class'];
$newPipeLine[] = is_string($processor) ? new $processor() : $processor;
}
}
}
);
$generator->setProcessorPipeline(new Pipeline($newPipeLine));
}
/**
* Set the analyzer for the OpenAPI generator based on scan options.
*
* @param OpenApiGenerator $generator The OpenAPI generator instance.
* @return void
*/
protected function setAnalyser(OpenApiGenerator $generator): void
{
$analyser = Arr::get($this->scanOptions, self::SCAN_OPTION_ANALYSER);
if (! empty($analyser)) {
$generator->setAnalyser($analyser);
return;
}
// Use AttributeAnnotationFactory for PHP 8.1+ native attributes
$generator->setAnalyser(new \OpenApi\Analysers\ReflectionAnalyser([
new \OpenApi\Analysers\AttributeAnnotationFactory(),
]));
}
/**
* Create and return a Finder instance configured for scanning directories.
*
* @return Finder
*/
protected function createScanFinder(): Finder
{
$pattern = Arr::get($this->scanOptions, self::SCAN_OPTION_PATTERN) ?: '*.php';
$exclude = Arr::get($this->scanOptions, self::SCAN_OPTION_EXCLUDE);
$exclude = ! empty($exclude) ? $exclude : $this->excludedDirs;
return new SourceFinder($this->annotationsDir, $exclude, $pattern);
}
/**
* Populate the servers list in the OpenAPI configuration using the base path.
*
* @return self
*/
protected function populateServers(): self
{
if ($this->basePath !== null && $this->openApi !== null) {
if (
$this->openApi->servers === OpenApiGenerator::UNDEFINED // @phpstan-ignore-line
|| is_array($this->openApi->servers) === false // @phpstan-ignore-line
) {
$this->openApi->servers = [];
}
$this->openApi->servers[] = new Server(['url' => $this->basePath]);
}
return $this;
}
/**
* Saves the JSON data and applies security measures to the file.
*
* @return self
*
* @throws FileNotFoundException
* @throws OpenApiException
*/
protected function saveJson(): self
{
if ($this->openApi !== null) {
$this->openApi->saveAs($this->docsFile);
}
$this->security->generate($this->docsFile);
return $this;
}
/**
* Creates a YAML copy of the OpenAPI documentation if required.
*
* This method converts the JSON documentation file to YAML format and saves it
* to the specified file path when the YAML copy requirement is enabled.
*
* @return void
*
* @throws FileNotFoundException
*/
protected function makeYamlCopy(): void
{
if ($this->yamlCopyRequired) {
$yamlDocs = (new YamlDumper(2))->dump(
json_decode($this->fileSystem->get($this->docsFile), true),
20,
0,
Yaml::DUMP_OBJECT_AS_MAP ^ Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE
);
$this->fileSystem->put(
$this->yamlDocsFile,
$yamlDocs
);
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace L5Swagger;
use L5Swagger\Exceptions\L5SwaggerException;
class GeneratorFactory
{
public function __construct(private readonly ConfigFactory $configFactory)
{
}
/**
* Creates and returns a new Generator instance based on the provided documentation configuration.
*
* @param string $documentation The name or identifier of the documentation to generate.
* @return Generator The configured Generator instance.
*
* @throws L5SwaggerException
*/
public function make(string $documentation): Generator
{
$config = $this->configFactory->documentationConfig($documentation);
$paths = $config['paths'];
$scanOptions = $config['scanOptions'] ?? [];
$constants = $config['constants'] ?? [];
$yamlCopyRequired = $config['generate_yaml_copy'] ?? false;
$secSchemesConfig = $config['securityDefinitions']['securitySchemes'] ?? [];
$secConfig = $config['securityDefinitions']['security'] ?? [];
$security = new SecurityDefinitions($secSchemesConfig, $secConfig);
return new Generator(
$paths,
$constants,
$yamlCopyRequired,
$security,
$scanOptions
);
}
}
@@ -0,0 +1,50 @@
<?php
namespace L5Swagger\Http\Controllers;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
use L5Swagger\Exceptions\L5SwaggerException;
/**
* Handles requests for serving Swagger UI assets.
*/
class SwaggerAssetController extends BaseController
{
/**
* Serves a specific documentation asset for the Swagger UI interface.
*
* @param Request $request The incoming HTTP request, which includes parameters to locate the requested asset.
* @return Response The HTTP response containing the requested asset content
* or a 404 error if the asset is not found.
*
* @throws FileNotFoundException
*/
public function index(Request $request): Response
{
$fileSystem = new Filesystem();
$documentation = $request->offsetGet('documentation');
$asset = $request->offsetGet('asset');
try {
$path = swagger_ui_dist_path($documentation, $asset);
return (new Response(
$fileSystem->get($path),
200,
[
'Content-Type' => (isset(pathinfo($asset)['extension']) && pathinfo($asset)['extension'] === 'css')
? 'text/css'
: 'application/javascript',
]
))->setSharedMaxAge(31536000)
->setMaxAge(31536000)
->setExpires(new \DateTime('+1 year'));
} catch (L5SwaggerException $exception) {
return abort(404, $exception->getMessage());
}
}
}
@@ -0,0 +1,201 @@
<?php
namespace L5Swagger\Http\Controllers;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Request as RequestFacade;
use L5Swagger\ConfigFactory;
use L5Swagger\Exceptions\L5SwaggerException;
use L5Swagger\GeneratorFactory;
class SwaggerController extends BaseController
{
public function __construct(
private readonly GeneratorFactory $generatorFactory,
private readonly ConfigFactory $configFactory
) {
}
/**
* Handles requests for API documentation and returns the corresponding file content.
*
* @param Request $request The HTTP request containing parameters such as documentation and configuration.
* @return Response The HTTP response containing the documentation file content with appropriate headers.
*
* @throws FileNotFoundException If the documentation file does not exist.
* @throws Exception If the documentation generation process fails.
*/
public function docs(Request $request): Response
{
$fileSystem = new Filesystem();
$documentation = $request->offsetGet('documentation');
$config = $request->offsetGet('config');
$formatToUseForDocs = $config['paths']['format_to_use_for_docs'] ?? 'json';
$yamlFormat = ($formatToUseForDocs === 'yaml');
$filePath = sprintf(
'%s/%s',
$config['paths']['docs'],
$yamlFormat ? $config['paths']['docs_yaml'] : $config['paths']['docs_json']
);
if ($config['generate_always']) {
$generator = $this->generatorFactory->make($documentation);
try {
$generator->generateDocs();
} catch (Exception $e) {
Log::error($e);
abort(
404,
sprintf(
'Unable to generate documentation file to: "%s". Please make sure directory is writable. Error: %s',
$filePath,
$e->getMessage()
)
);
}
}
if (! $fileSystem->exists($filePath)) {
abort(404, sprintf('Unable to locate documentation file at: "%s"', $filePath));
}
$content = $fileSystem->get($filePath);
if ($yamlFormat) {
return response($content, 200, [
'Content-Type' => 'application/yaml',
'Content-Disposition' => 'inline',
]);
}
return response($content, 200, [
'Content-Type' => 'application/json',
]);
}
/**
* Handles the API request and renders the Swagger documentation view.
*
* @param Request $request The HTTP request containing necessary parameters such as documentation and configuration details.
* @return Response The HTTP response rendering the Swagger documentation view.
*/
public function api(Request $request): Response
{
$documentation = $request->offsetGet('documentation');
$config = $request->offsetGet('config');
$proxy = $config['proxy'];
if ($proxy) {
if (! is_array($proxy)) {
$proxy = [$proxy];
}
Request::setTrustedProxies(
$proxy,
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB
);
}
$urlToDocs = $this->generateDocumentationFileURL($documentation, $config);
$urlsToDocs = $this->getAllDocumentationUrls();
$useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true);
// Need the / at the end to avoid CORS errors on Homestead systems.
return response(
view('l5-swagger::index', [
'documentation' => $documentation,
'documentationTitle' => $config['api']['title'] ?? $documentation,
'secure' => RequestFacade::secure(),
'urlToDocs' => $urlToDocs, // Is not used in the view, but still passed for backwards compatibility
'urlsToDocs' => $urlsToDocs,
'operationsSorter' => $config['operations_sort'],
'configUrl' => $config['additional_config_url'],
'validatorUrl' => $config['validator_url'],
'useAbsolutePath' => $useAbsolutePath,
]),
200
);
}
/**
* Handles the OAuth2 callback and retrieves the required file for the redirect.
*
* @param Request $request The HTTP request containing necessary parameters.
* @return string The content of the OAuth2 redirect file.
*
* @throws FileNotFoundException
* @throws L5SwaggerException
*/
public function oauth2Callback(Request $request): string
{
$fileSystem = new Filesystem();
$documentation = $request->offsetGet('documentation');
return $fileSystem->get(swagger_ui_dist_path($documentation, 'oauth2-redirect.html'));
}
/**
* Generate the URL for accessing the documentation file based on the provided configuration.
*
* @param string $documentation The name of the documentation instance.
* @param array<string,mixed> $config The configuration settings for generating the documentation URL.
* @return string The generated URL for the documentation file.
*/
protected function generateDocumentationFileURL(string $documentation, array $config): string
{
$fileUsedForDocs = $config['paths']['docs_json'] ?? 'api-docs.json';
if (! empty($config['paths']['format_to_use_for_docs'])
&& $config['paths']['format_to_use_for_docs'] === 'yaml'
&& $config['paths']['docs_yaml']
) {
$fileUsedForDocs = $config['paths']['docs_yaml'];
}
$useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true);
return route(
'l5-swagger.'.$documentation.'.docs',
$fileUsedForDocs,
$useAbsolutePath
);
}
/**
* Retrieves all available documentation URLs with their corresponding titles.
*
* @return array<string,string> An associative array where the keys are documentation titles
* and the values are the corresponding URLs.
*
* @throws L5SwaggerException
*/
protected function getAllDocumentationUrls(): array
{
/** @var array<string> $documentations */
$documentations = array_keys(config('l5-swagger.documentations', []));
$urlsToDocs = [];
foreach ($documentations as $documentationName) {
$config = $this->configFactory->documentationConfig($documentationName);
$title = $config['api']['title'] ?? $documentationName;
$urlsToDocs[$title] = $this->generateDocumentationFileURL($documentationName, $config);
}
return $urlsToDocs;
}
}
@@ -0,0 +1,38 @@
<?php
namespace L5Swagger\Http\Middleware;
use Closure;
use L5Swagger\ConfigFactory;
use L5Swagger\Exceptions\L5SwaggerException;
class Config
{
public function __construct(
private readonly ConfigFactory $configFactory
) {
}
/**
* Handles the incoming request by extracting documentation configuration and setting it on the request.
*
* @param mixed $request The incoming HTTP request.
* @param Closure $next The next middleware in the pipeline.
* @return mixed The processed HTTP response after passing through the next middleware.
*
* @throws L5SwaggerException
*/
public function handle(mixed $request, Closure $next): mixed
{
$actions = $request->route()->getAction();
$documentation = $actions['l5-swagger.documentation'];
$config = $this->configFactory->documentationConfig($documentation);
$request->offsetSet('documentation', $documentation);
$request->offsetSet('config', $config);
return $next($request);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace L5Swagger;
use Illuminate\Support\Facades\Facade;
class L5SwaggerFacade extends Facade
{
/**
* Get the registered name of the component being accessed through the facade.
*
* @codeCoverageIgnore
*
* @return string The name or class of the underlying component.
*/
protected static function getFacadeAccessor(): string
{
return Generator::class;
}
}
@@ -0,0 +1,75 @@
<?php
namespace L5Swagger;
use Illuminate\Support\ServiceProvider;
use L5Swagger\Console\GenerateDocsCommand;
class L5SwaggerServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot(): void
{
$viewPath = __DIR__.'/../resources/views';
$this->loadViewsFrom($viewPath, 'l5-swagger');
// Publish a config file
$configPath = __DIR__.'/../config/l5-swagger.php';
$this->publishes([
$configPath => config_path('l5-swagger.php'),
], 'config');
//Publish views
$this->publishes([
__DIR__.'/../resources/views' => config('l5-swagger.defaults.paths.views'),
], 'views');
//Include routes
$this->loadRoutesFrom(__DIR__.'/routes.php');
//Register commands
$this->commands([GenerateDocsCommand::class]);
}
/**
* Register the service provider.
*
* @return void
*/
public function register(): void
{
$configPath = __DIR__.'/../config/l5-swagger.php';
$this->mergeConfigFrom($configPath, 'l5-swagger');
$this->app->singleton('command.l5-swagger.generate', function ($app) {
return $app->make(GenerateDocsCommand::class);
});
$this->app->bind(Generator::class, function ($app) {
$documentation = config('l5-swagger.default');
/** @var GeneratorFactory $factory */
$factory = $app->make(GeneratorFactory::class);
return $factory->make($documentation);
});
}
/**
* Get the services provided by the provider.
*
* @codeCoverageIgnore
*
* @return array<string>
*/
public function provides(): array
{
return [
'command.l5-swagger.generate',
];
}
}
@@ -0,0 +1,117 @@
<?php
namespace L5Swagger;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
class SecurityDefinitions
{
/**
* @param array<string,mixed> $schemasConfig
* @param array<string,mixed> $securityConfig
*/
public function __construct(
private readonly array $schemasConfig = [],
private readonly array $securityConfig = []
) {
}
/**
* Reads in the l5-swagger configuration and appends security settings to documentation.
*
* @param string $filename The path to the generated json documentation
*
* @throws FileNotFoundException
*/
public function generate(string $filename): void
{
$fileSystem = new Filesystem();
$documentation = collect(
json_decode($fileSystem->get($filename))
);
if (! empty($this->schemasConfig)) {
$documentation = $this->injectSecuritySchemes($documentation, $this->schemasConfig);
}
if (! empty($this->securityConfig)) {
$documentation = $this->injectSecurity($documentation, $this->securityConfig);
}
$fileSystem->put(
$filename,
$documentation->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
);
}
/**
* Inject security schemes settings.
*
* @param Collection<int|string, mixed> $documentation The parse json
* @param array<string,mixed> $config The securityScheme settings from l5-swagger
* @return Collection<int|string, mixed>
*/
protected function injectSecuritySchemes(Collection $documentation, array $config): Collection
{
$components = collect();
if ($documentation->has('components')) {
$components = collect($documentation->get('components'));
}
$securitySchemes = collect();
if ($components->has('securitySchemes')) {
$securitySchemes = collect($components->get('securitySchemes'));
}
foreach ($config as $key => $cfg) {
$securitySchemes->offsetSet($key, $this->arrayToObject($cfg));
}
$components->offsetSet('securitySchemes', $securitySchemes);
$documentation->offsetSet('components', $components);
return $documentation;
}
/**
* Inject security settings.
*
* @param Collection<int|string, mixed> $documentation The parse json
* @param array<string, mixed> $config The security settings from l5-swagger
* @return Collection<int|string, mixed>
*/
protected function injectSecurity(Collection $documentation, array $config): Collection
{
$security = collect();
if ($documentation->has('security')) {
$security = collect($documentation->get('security'));
}
foreach ($config as $key => $cfg) {
if (! empty($cfg)) {
$security->put($key, $this->arrayToObject($cfg));
}
}
if ($security->count()) {
$documentation->offsetSet('security', $security);
}
return $documentation;
}
/**
* Converts an array to an object.
*
* @param $array<string, mixed>
* @return object
*/
protected function arrayToObject(mixed $array): mixed
{
return json_decode(json_encode($array) ?: '');
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
use L5Swagger\Exceptions\L5SwaggerException;
if (! function_exists('swagger_ui_dist_path')) {
/**
* Returns swagger-ui composer dist path.
*
* @param string $documentation
* @param string|null $asset
* @return string
*
* @throws L5SwaggerException
*/
function swagger_ui_dist_path(string $documentation, ?string $asset = null): string
{
$allowedFiles = [
'favicon-16x16.png',
'favicon-32x32.png',
'oauth2-redirect.html',
'swagger-ui-bundle.js',
'swagger-ui-standalone-preset.js',
'swagger-ui.css',
'swagger-ui.js',
];
$defaultPath = 'vendor/swagger-api/swagger-ui/dist/';
$path = base_path(
config('l5-swagger.documentations.'.$documentation.'.paths.swagger_ui_assets_path', $defaultPath)
);
if (! $asset) {
return realpath($path) ?: '';
}
if (! in_array($asset, $allowedFiles, true)) {
throw new L5SwaggerException(sprintf('(%s) - this L5 Swagger asset is not allowed', $asset));
}
return realpath($path.$asset) ?: '';
}
}
if (! function_exists('l5_swagger_asset')) {
/**
* Returns asset from swagger-ui composer package.
*
* @param string $documentation
* @param $asset string
* @return string
*
* @throws L5SwaggerException
*/
function l5_swagger_asset(string $documentation, string $asset): string
{
$file = swagger_ui_dist_path($documentation, $asset);
if (! file_exists($file)) {
throw new L5SwaggerException(sprintf('Requested L5 Swagger asset file (%s) does not exists', $asset));
}
$useAbsolutePath = config('l5-swagger.documentations.'.$documentation.'.paths.use_absolute_path', true);
return route('l5-swagger.'.$documentation.'.asset', $asset, $useAbsolutePath).'?v='.md5_file($file);
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
use L5Swagger\ConfigFactory;
use L5Swagger\Http\Middleware\Config as L5SwaggerConfig;
Route::group(['namespace' => 'L5Swagger'], static function (Router $router) {
$configFactory = resolve(ConfigFactory::class);
/** @var array<string,string> $documentations */
$documentations = config('l5-swagger.documentations', []);
foreach (array_keys($documentations) as $name) {
$config = $configFactory->documentationConfig($name);
if (! isset($config['routes'])) {
continue;
}
$groupOptions = $config['routes']['group_options'] ?? [];
if (! isset($groupOptions['middleware'])) {
$groupOptions['middleware'] = [];
}
if (is_string($groupOptions['middleware'])) {
$groupOptions['middleware'] = [$groupOptions['middleware']];
}
$groupOptions['l5-swagger.documentation'] = $name;
$groupOptions['middleware'][] = L5SwaggerConfig::class;
Route::group($groupOptions, static function (Router $router) use ($name, $config) {
if (isset($config['routes']['api'])) {
$router->get($config['routes']['api'], [
'as' => 'l5-swagger.'.$name.'.api',
'middleware' => $config['routes']['middleware']['api'] ?? [],
'uses' => '\L5Swagger\Http\Controllers\SwaggerController@api',
]);
}
if (isset($config['routes']['docs'])) {
$router->get($config['routes']['docs'], [
'as' => 'l5-swagger.'.$name.'.docs',
'middleware' => $config['routes']['middleware']['docs'] ?? [],
'uses' => '\L5Swagger\Http\Controllers\SwaggerController@docs',
]);
$router->get($config['routes']['docs'].'/asset/{asset}', [
'as' => 'l5-swagger.'.$name.'.asset',
'middleware' => $config['routes']['middleware']['asset'] ?? [],
'uses' => '\L5Swagger\Http\Controllers\SwaggerAssetController@index',
]);
}
if (isset($config['routes']['oauth2_callback'])) {
$router->get($config['routes']['oauth2_callback'], [
'as' => 'l5-swagger.'.$name.'.oauth2_callback',
'middleware' => $config['routes']['middleware']['oauth2_callback'] ?? [],
'uses' => '\L5Swagger\Http\Controllers\SwaggerController@oauth2Callback',
]);
}
});
}
});