esportscz / swagger
OpenAPI specification generator and Swagger UI for PHP 7.2-8.4 projects (standalone and Nette Framework)
Requires
- php: ^7.2 || ^8.0
- doctrine/annotations: ^1.7 || ^2.0
- psr/simple-cache: ^1.0
- swagger-api/swagger-ui: ^5.0
- zircote/swagger-php: ^3.7 || ^4.0
Requires (Dev)
- nette/application: ^3.1
- nette/di: ^3.0.0 <3.2
- nette/tester: ^2.4
- orisai/coding-standard: ^3.0
- phpstan/phpstan: ^1.0
Suggests
- nette/application: ^3.1 — required for Nette route wiring (SwaggerExtension)
- nette/di: ^3.0 — required for Nette DI extension (SwaggerExtension)
- symfony/yaml: Required for YAML output format (^4.4 || ^5.4 || ^6.4 || ^7.0)
This package is auto-updated.
Last update: 2026-03-28 09:13:40 UTC
README
Internal Composer package for standardised OpenAPI specification generation and Swagger UI across company PHP projects. Provides a unified interface for generating OpenAPI documentation from annotations/attributes and displaying an interactive Swagger UI.
The package is modular - you can use only the OpenAPI generator, only the Swagger UI, or both together.
Features
- OpenAPI 3.0 specification generation from PHP doctrine annotations (PHP 7.2+) and native attributes (PHP 8.x)
- JSON and YAML output formats
- Bundled Swagger UI assets (offline-capable, no CDN dependency)
- Automatic cache with filemtime-based invalidation
- PSR-16 (SimpleCache) cache adapter support
- Standalone plain-PHP handler - no framework required
- Nette DI extension with NEON configuration
- Independent access guards for UI and spec routes
- Fully configurable Swagger UI options
Requirements
- PHP
^7.2 || ^8.0 - Composer
Optional:
symfony/yaml- required for YAML output formatnette/di ^3.0- required for Nette DI extensionnette/application ^3.1- required for Nette route wiring
Installation
composer require esportscz/swagger
For YAML output support:
composer require symfony/yaml
Nette Integration
Requires nette/di ^3.0 (install it with composer require nette/di).
Minimal Configuration
extensions:
swagger: eSportsCZ\Swagger\DI\SwaggerExtension
swagger:
# Directories to scan for OpenAPI annotations / PHP 8 attributes
# When omitted or empty, PSR-4 auto-discovery from composer.json is used as fallback
scanPaths:
- %appDir%/Api
Full NEON Configuration
extensions:
swagger: eSportsCZ\Swagger\DI\SwaggerExtension
swagger:
# Directories to scan for OpenAPI annotations / PHP 8 attributes
# When omitted or empty, PSR-4 auto-discovery from composer.json is used as fallback
scanPaths:
- %appDir%/Api
- %appDir%/Controllers
# Directories to exclude from scanning (default: [])
excludePaths:
- %appDir%/Api/Internal
# Page title shown in Swagger UI header and browser tab (default: 'API Documentation')
title: My Application API
# OpenAPI specification version: '3.0' or '3.1' (default: '3.0')
# OpenAPI 3.1 requires swagger-php v4
version: '3.0'
# Output format for the spec endpoint: 'json' or 'yaml' (default: 'json')
# YAML requires: composer require symfony/yaml
outputFormat: json
# Directory for the file-based spec cache (default: %tempDir%/swagger)
cachePath: %tempDir%/swagger
# Enable or disable caching (default: true)
# When a PSR-16 CacheInterface service is registered in the container,
# FileCacheAdapter is automatically replaced with Psr16CacheAdapter
cacheEnabled: true
# Persist Swagger UI auth tokens across page reloads (default: true)
persistAuthorization: true
# Enable the "Try it out" button in Swagger UI by default (default: true)
tryItOut: true
# Initial expansion state of operations and tags: 'none' | 'list' | 'full' (default: 'list')
docExpansion: list
# Arbitrary SwaggerUIBundle options as key-value pairs (default: {})
# Merged on top of the typed options above - uiOptions win on conflict
uiOptions:
filter: true
deepLinking: true
# Route prefix for Nette router wiring (default: '/swagger')
# Registers: {prefix}, {prefix}/spec.json, {prefix}/spec.yaml, {prefix}/assets/<file>
routePrefix: /swagger
# OpenAPI generation module
openapi:
# Enable OpenAPI spec generation - registers SpecGenerator + cache (default: true)
enabled: true
# Swagger UI module
ui:
# Enable Swagger UI - registers UiRenderer, AssetLocator and routes (default: true)
enabled: true
# External spec URL - overrides the default /spec.json path (default: null)
# Required when openapi.enabled is false and ui.enabled is true
specUrl: null
NEON Configuration Reference
| Key | Type | Default | Description |
|---|---|---|---|
scanPaths | string[] | [] | Directories to scan (empty = PSR-4 auto-discovery) |
excludePaths | string[] | [] | Directories to exclude |
title | string | 'API Documentation' | Page title |
version | string | '3.0' | OpenAPI version |
outputFormat | json\|yaml | json | Output format |
cachePath | string\|null | null | Cache directory path |
cacheEnabled | bool | true | Enable/disable caching |
persistAuthorization | bool | true | Persist auth tokens |
tryItOut | bool | true | Enable Try It Out |
docExpansion | none\|list\|full | list | Initial expansion state |
uiOptions | array | [] | Extra SwaggerUIBundle options |
routePrefix | string | '/swagger' | Route prefix for Nette router |
openapi.enabled | bool | true | Enable OpenAPI spec generation (SpecGenerator + cache) |
ui.enabled | bool | true | Enable Swagger UI (UiRenderer + AssetLocator + routes) |
ui.specUrl | string\|null | null | External spec URL for UI (overrides /spec.json) |
Modular Configuration
Disable modules you don't need:
# Generator only - no UI services or routes
swagger:
scanPaths:
- %appDir%/Api
ui:
enabled: false
# UI only - point at an external spec, no local generator
swagger:
scanPaths: []
openapi:
enabled: false
ui:
specUrl: 'https://api.example.com/openapi.json'
Note: When
openapi.enabled: falseandui.enabled: true, theui.specUrloption is required - the extension throwsInvalidStateExceptionif it is not set.
PSR-16 Cache Auto-Discovery
When a Psr\SimpleCache\CacheInterface service exists in the DI container (registered by your application), SwaggerExtension automatically replaces the default FileCacheAdapter with Psr16CacheAdapter wrapping your PSR-16 instance.
No configuration is needed - the swap happens transparently in beforeCompile().
Presenter
When nette/application is installed, the extension automatically:
- Registers
DefaultPresenteras a DI service (handles all Swagger routes) - Sets up module mapping
Swagger->eSportsCZ\Swagger\Presenter\*PresenterinPresenterFactory
No manual presenter creation is needed. After registering the extension and configuring scanPaths, Swagger UI is available at routePrefix (default: /swagger).
The presenter handles 4 actions:
| Action | Route | Description |
|---|---|---|
ui | {routePrefix} | Renders Swagger UI HTML page |
specJson | {routePrefix}/spec.json | Returns OpenAPI spec as JSON |
specYaml | {routePrefix}/spec.yaml | Returns OpenAPI spec as YAML |
asset | {routePrefix}/assets/<file> | Serves bundled Swagger UI static assets |
When openapi.enabled: false (UI-only mode with ui.specUrl), the spec actions redirect to the configured external URL.
Route Wiring
When Nette\Routing\Router is found in the DI container, the extension automatically registers routes under routePrefix. If no Router service is found, route wiring is silently skipped.
Standalone Usage
Drop a swagger.php file into your project's public directory:
<?php
require __DIR__ . '/../vendor/autoload.php';
use eSportsCZ\Swagger\SwaggerConfig;
use eSportsCZ\Swagger\Spec\SpecGenerator;
use eSportsCZ\Swagger\Ui\AssetLocator;
use eSportsCZ\Swagger\Ui\UiRenderer;
use eSportsCZ\Swagger\Handler\StandaloneHandler;
$config = (new SwaggerConfig())
->setScanPaths([__DIR__ . '/../src/Api'])
->setTitle('My API')
->setCachePath(__DIR__ . '/../temp/swagger');
$generator = new SpecGenerator($config);
$locator = new AssetLocator();
$renderer = new UiRenderer($locator);
$handler = new StandaloneHandler($config, $generator, $renderer, $locator);
$handler->handle();
Routing
StandaloneHandler::handle() reads $_SERVER['REQUEST_URI'] and dispatches to the appropriate response:
| Path | Response |
|---|---|
/ or /index | Swagger UI HTML page |
/spec.json | OpenAPI spec (JSON) |
/spec.yaml | OpenAPI spec (YAML) |
/assets/* | Bundled Swagger UI static assets (JS, CSS, PNG) |
| Any other path | 404 plain text |
Access Guards
Protect the UI or the spec endpoint independently:
$handler = new StandaloneHandler($config, $generator, $renderer, $locator);
// Protect UI with HTTP Basic Auth
$handler->setUiGuard(function () {
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
$pass = $_SERVER['PHP_AUTH_PW'] ?? '';
return $user === 'admin' && $pass === 'secret';
});
// Keep spec endpoint public (e.g. for API clients)
// $handler->setSpecGuard(function () { return isApiClient(); });
$handler->handle();
Both guards default to null, which means public access by default. A guard returning false triggers a 403 Forbidden response.
Configuration Reference
All configuration is done via SwaggerConfig, a fluent builder:
$config = (new SwaggerConfig())
->setScanPaths([...])
->setTitle('My API');
setScanPaths(array $paths): self
Directories to scan for OpenAPI annotations/attributes.
- Default:
[] - Note: When empty, auto-discovery reads PSR-4 namespaces from
composer.json. When explicit paths are set, PSR-4 auto-discovery is skipped entirely.
$config->setScanPaths([
__DIR__ . '/src/Api',
__DIR__ . '/src/Controller',
]);
setExcludePaths(array $paths): self
Directories to exclude from scanning (applied after path resolution).
- Default:
[]
$config->setExcludePaths([
__DIR__ . '/src/Api/Internal',
]);
setTitle(string $title): self
Page title shown in the browser tab and Swagger UI header.
- Default:
'API Documentation'
$config->setTitle('My Application API');
setVersion(string $version): self
OpenAPI specification version to generate.
- Default:
'3.0' - Accepted values:
'3.0'or'3.1'(3.1 requires swagger-php v4)
$config->setVersion('3.1');
setOutputFormat(string $format): self
Serialisation format for the generated spec.
- Default:
'json' - Accepted values:
'json'or'yaml' - Note:
'yaml'requiressymfony/yamlinstalled.
$config->setOutputFormat('yaml');
setCachePath(string $path): self
Directory where generated specs are stored by FileCacheAdapter.
- Default:
null(falls back tosys_get_temp_dir() . '/swagger-cache')
$config->setCachePath(__DIR__ . '/temp/swagger');
disableCache(): self
Disable caching entirely. The spec is regenerated on every request.
- Default cache state: enabled
$config->disableCache();
setPersistAuthorization(bool $value): self
When true, Swagger UI persists authorization tokens across page reloads.
- Default:
true
$config->setPersistAuthorization(false);
setTryItOut(bool $value): self
When true, the "Try it out" button is enabled in Swagger UI by default.
- Default:
true
$config->setTryItOut(false);
setDocExpansion(string $value): self
Controls the initial expansion state of operations and tags.
- Default:
'list' - Accepted values:
'none'|'list'|'full'
$config->setDocExpansion('none'); // collapse everything on load
setUiOptions(array $options): self
Pass arbitrary SwaggerUIBundle options as a key-value array. These are merged on top of the typed setters, so uiOptions values take precedence.
- Default:
[]
$config->setUiOptions([
'filter' => true,
'deepLinking' => true,
'maxDisplayedTags' => 5,
]);
setSpecUrl(?string $url): self
Set a custom spec URL for Swagger UI. When set, the UI loads the OpenAPI specification from this URL instead of the default /spec.json. Required for UI-only mode (openapi.enabled: false).
- Default:
null(uses/spec.json)
$config->setSpecUrl('https://api.example.com/openapi.json');
setOpenApiEnabled(bool $value): self
Enable or disable the OpenAPI generation module. When disabled, SpecGenerator and the cache service are not registered in Nette DI.
- Default:
true
$config->setOpenApiEnabled(false); // UI-only mode
setUiEnabled(bool $value): self
Enable or disable the Swagger UI module. When disabled, UiRenderer and AssetLocator are not registered in Nette DI and route wiring is skipped.
- Default:
true
$config->setUiEnabled(false); // generator-only mode
OpenAPI Annotations
PHP 7.x - Doctrine Annotations
<?php
use OpenApi\Annotations as OA;
/**
* @OA\Get(
* path="/users",
* summary="List all users",
* @OA\Response(response=200, description="Success")
* )
*/
class UserController
{
// ...
}
PHP 8.x - Native Attributes
<?php
use OpenApi\Attributes as OA;
class UserController
{
#[OA\Get(path: '/users', summary: 'List all users')]
#[OA\Response(response: 200, description: 'Success')]
public function index(): void
{
// ...
}
}
Full annotation reference: zircote/swagger-php documentation
Cache
Default File Cache
When cacheEnabled is true (default) and setCachePath() is configured, specs are stored as files in the specified directory:
$config = (new SwaggerConfig())
->setScanPaths([__DIR__ . '/src/Api'])
->setCachePath(__DIR__ . '/temp/swagger');
If setCachePath() is not called, files are stored in sys_get_temp_dir() . '/swagger-cache'.
Cache Invalidation
Cache keys are derived from:
- File modification times (
filemtime) of all scanned source files - Configuration hash (version, output format, exclude paths)
The cache is automatically invalidated when any source file changes or config changes.
PSR-16 Adapter
Inject any PSR-16 compatible cache (Redis, Memcached, APCu, etc.):
use eSportsCZ\Swagger\Cache\Psr16CacheAdapter;
$psr16Cache = new YourApp\RedisCache(); // implements Psr\SimpleCache\CacheInterface
$cacheAdapter = new Psr16CacheAdapter($psr16Cache);
$generator = new SpecGenerator($config, null, $cacheAdapter);
Disabling Cache
$config = (new SwaggerConfig())
->setScanPaths([__DIR__ . '/src/Api'])
->disableCache();
Module Independence
The package is split into three independent modules that can be used separately:
MOD-01: OpenAPI Generator Only (no UI)
Use SpecGenerator without any UI components:
use eSportsCZ\Swagger\SwaggerConfig;
use eSportsCZ\Swagger\Spec\SpecGenerator;
$config = (new SwaggerConfig())
->setScanPaths([__DIR__ . '/src/Api'])
->setOutputFormat('json');
$generator = new SpecGenerator($config);
$json = $generator->generate();
header('Content-Type: application/json');
echo $json;
Use case: Expose the spec for external tooling (Postman, code generation) without hosting a UI.
MOD-02: Swagger UI Only (external spec URL)
Use UiRenderer with an external spec URL - no local generator needed:
use eSportsCZ\Swagger\Ui\AssetLocator;
use eSportsCZ\Swagger\Ui\UiRenderer;
use eSportsCZ\Swagger\SwaggerConfig;
$config = (new SwaggerConfig())->setTitle('External API Docs');
$locator = new AssetLocator();
$renderer = new UiRenderer($locator);
$html = $renderer->render(
$config,
'https://api.example.com/spec.json', // external spec URL
'/assets/swagger-ui.css',
'/assets/swagger-ui-bundle.js',
'/assets/swagger-ui-standalone-preset.js'
);
header('Content-Type: text/html; charset=UTF-8');
echo $html;
Use case: Host a UI pointing at a spec generated elsewhere.
MOD-03: Generator + UI Without Handler
Wire SpecGenerator and UiRenderer without StandaloneHandler (e.g. in a framework controller):
use eSportsCZ\Swagger\SwaggerConfig;
use eSportsCZ\Swagger\Spec\SpecGenerator;
use eSportsCZ\Swagger\Ui\AssetLocator;
use eSportsCZ\Swagger\Ui\UiRenderer;
$config = (new SwaggerConfig())->setScanPaths([__DIR__ . '/src/Api']);
$generator = new SpecGenerator($config);
$locator = new AssetLocator();
$renderer = new UiRenderer($locator);
// In your router/controller:
if ($request->getPath() === '/api/spec.json') {
return new JsonResponse($generator->generate());
}
if ($request->getPath() === '/api/docs') {
$html = $renderer->render($config, '/api/spec.json', ...);
return new HtmlResponse($html);
}
Examples
See the examples/ directory for ready-to-use example files:
| File | Description |
|---|---|
examples/standalone.php | Minimal standalone setup |
examples/standalone-with-guards.php | Standalone with UI access guard |
examples/nette-config.neon | Minimal Nette NEON configuration |
examples/nette-config-full.neon | Full Nette NEON configuration with all options |
examples/generator-only.php | OpenAPI generation without UI (MOD-01) |
examples/custom-cache.php | PSR-16 cache adapter injection |
Development
Docker setup
docker-compose up -d
docker-compose exec php bash
Running tests
composer check:tests
Running static analysis
composer check:phpstan
Running coding standard
composer check:phpcs
To auto-fix coding standard violations:
composer check:phpcbf
All checks
composer check