Update Database dan ALL yang di butuhkan
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+50
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user