esportscz/swagger

OpenAPI specification generator and Swagger UI for PHP 7.2-8.4 projects (standalone and Nette Framework)

Maintainers

Package info

bitbucket.org/esportscz/swagger

Issues

pkg:composer/esportscz/swagger

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

1.0.1 2026-03-27 15:21 UTC

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 format
  • nette/di ^3.0 - required for Nette DI extension
  • nette/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

KeyTypeDefaultDescription
scanPathsstring[][]Directories to scan (empty = PSR-4 auto-discovery)
excludePathsstring[][]Directories to exclude
titlestring'API Documentation'Page title
versionstring'3.0'OpenAPI version
outputFormatjson\|yamljsonOutput format
cachePathstring\|nullnullCache directory path
cacheEnabledbooltrueEnable/disable caching
persistAuthorizationbooltruePersist auth tokens
tryItOutbooltrueEnable Try It Out
docExpansionnone\|list\|fulllistInitial expansion state
uiOptionsarray[]Extra SwaggerUIBundle options
routePrefixstring'/swagger'Route prefix for Nette router
openapi.enabledbooltrueEnable OpenAPI spec generation (SpecGenerator + cache)
ui.enabledbooltrueEnable Swagger UI (UiRenderer + AssetLocator + routes)
ui.specUrlstring\|nullnullExternal 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: false and ui.enabled: true, the ui.specUrl option is required - the extension throws InvalidStateException if 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:

  1. Registers DefaultPresenter as a DI service (handles all Swagger routes)
  2. Sets up module mapping Swagger -> eSportsCZ\Swagger\Presenter\*Presenter in PresenterFactory

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:

ActionRouteDescription
ui{routePrefix}Renders Swagger UI HTML page
specJson{routePrefix}/spec.jsonReturns OpenAPI spec as JSON
specYaml{routePrefix}/spec.yamlReturns 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:

PathResponse
/ or /indexSwagger UI HTML page
/spec.jsonOpenAPI spec (JSON)
/spec.yamlOpenAPI spec (YAML)
/assets/*Bundled Swagger UI static assets (JS, CSS, PNG)
Any other path404 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' requires symfony/yaml installed.
$config->setOutputFormat('yaml');

setCachePath(string $path): self

Directory where generated specs are stored by FileCacheAdapter.

  • Default: null (falls back to sys_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:

FileDescription
examples/standalone.phpMinimal standalone setup
examples/standalone-with-guards.phpStandalone with UI access guard
examples/nette-config.neonMinimal Nette NEON configuration
examples/nette-config-full.neonFull Nette NEON configuration with all options
examples/generator-only.phpOpenAPI generation without UI (MOD-01)
examples/custom-cache.phpPSR-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