darkaonline/l5-swagger
L5-Swagger packages swagger-php and Swagger UI for Laravel, letting you generate and serve OpenAPI/Swagger documentation for your API with minimal setup and a simple config file.
1. Install and publish (5 minutes).
composer require darkaonline/l5-swagger
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"
You now have config/l5-swagger.php and a docs folder at storage/api-docs. The published README is a link-farm to the Wiki — read Installation & Configuration first, then config/l5-swagger.php top to bottom; that file is the documentation for every knob (routes, paths, scan options, security schemes, UI flags).
2. Look at the config before writing any docblocks. The three keys that decide your life:
documentations.default.paths.docs → storage_path('api-docs') (generated JSON/YAML lives here)documentations.default.paths.annotations / scanOptions.paths → what gets scanned (default base_path('app'))documentations.default.routes.api → api/documentation (the Swagger UI URL)3. Define the root Info once. The spec is only valid if swagger-php finds an OA\Info somewhere inside a scanned path. Dump it on your base controller (or a dedicated App\OpenApi\OpenApi class) — this is the very first thing to get right, because everything else fails until it exists:
namespace App\Http\Controllers;
use OpenApi\Attributes as OA;
#[OA\Info(version: '1.0.0', title: 'My API', description: 'Internal API')]
#[OA\Server(url: '/api', description: 'Default')]
#[OA\SecurityScheme(
securityScheme: 'bearerAuth',
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
)]
abstract class Controller
{
//
}
4. Annotate one endpoint and generate.
#[OA\Get(
path: '/users/{id}',
summary: 'Show a user',
security: [['bearerAuth' => []]],
tags: ['Users'],
parameters: [new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer'))],
responses: [
new OA\Response(response: 200, description: 'OK',
content: new OA\JsonContent(ref: '#/components/schemas/User')),
new OA\Response(response: 404, description: 'Not found'),
]
)]
public function show(User $user) { /* ... */ }
php artisan l5-swagger:generate
open http://myapp.test/api/documentation
That loop — annotate, l5-swagger:generate, refresh UI — is the whole day-to-day workflow. First real use case: get the spec in front of frontend/mobile consumers so they stop asking you what the response shape is, then feed the same JSON into Postman/Insomnia/client generators.
Attributes live on the controller, not in a parallel doc file. The idiomatic Laravel pattern is one OA\* attribute per action, OA\Schema attributes on API Resources / DTOs, and OA\Parameter for path/query params. Group with tags: ['Users'] and describe the error envelope once as a schema.
Reuse schemas via ref + ::class, not copy-paste. swagger-php resolves ref: UserResource::class when the class carries #[OA\Schema]; attribute arguments must be constant expressions, so use class constants for enums/shared strings:
#[OA\Schema(schema: 'User')]
class UserResource extends JsonResource { /* ... */ }
Domain-split large specs. Put the shared info/security in one class (App\OpenApi\OpenApi), per-domain path attributes in per-module classes, and either keep scanOptions.paths pointed at app_path() or narrow it to [app_path('Http/Controllers/Api'), app_path('OpenApi')] to keep generation fast. For totally separate audiences (public API vs. admin API), use multiple documentations entries in the config, each with its own paths and routes.api, then php artisan l5-swagger:generate <documentation> to build just that one.
Auth: configure security + securityDefinitions once. Sanctum/Passport both reduce to a bearerAuth scheme; add a global security entry in the config (or per-operation) and enable persist_authorization in the UI so testers don't re-paste tokens after every refresh. oauth2 with PKCE is supported via the UI config keys.
Generate in deploy, not on request. Set generate_always => false (production default) and add the generate step to your deploy script / CI job right after composer install:
php artisan l5-swagger:generate
# spec now at storage/api-docs/api-docs.json
Treat the spec as an artifact under test. In CI, generate and then lint the output with swagger-php's own validator (vendor/bin/openapi --validate) or assert the JSON parses and contains expected paths. This catches the "spec silently generated without Info" and "one controller's attributes threw and were skipped" classes of breakage before your consumers do.
Customize the UI via config, then views. Most UI behaviour (doc_expansion, filter, deepLinking, try_it_out, dark mode, format_to_use_for_docs = json|yaml) is env-driven through L5_SWAGGER_UI_* keys. When that's not enough, publish the package views (vendor:publish --tag=...) and override the template — it's a plain Blade file.
Protect or gate the docs route. Wrap routes.middleware with auth/can: or a basic-auth middleware, or set the api_key config so /api/documentation requires ?api_key=. Ship the spec internally and keep it out of the public surface.
doctrine/annotations was dropped in 10.0.0, so the legacy /** @OA\Get(...) */ docblock style no longer parses. Any tutorial or StackOverflow answer with @OA\ in a comment is for v8/v9. Use #[OA\...] attributes.OA\Info in a scanned path, (b) scanOptions.paths points somewhere your file isn't, or (c) your annotations sit on a file with no class (orphan annotations are silently ignored). Move the attribute onto a class.config() inside attributes — env(), now(), and string concatenation will blow up at generation time. Use class constants and L5_SWAGGER_CONST_HOST for the host.php artisan config:cache, changes to L5_SWAGGER_* env values won't apply until you re-cache. Conversely, if env vars appear ignored, check you're not cached.generate_always = true in production regenerates and re-scans on every page load — slow, and a needless attack surface. It's the #1 cause of "why is /api/documentation taking 8 seconds".OA\Server explicitly (or the constants config) rather than trusting X-Forwarded-*. Mixed-content "try it out" failures are usually this.php artisan route:cache — the package routes are registered but the cache may be stale; re-run route:clear && route:cache. Also confirm the route didn't collide with a catch-all {any} route.storage/api-docs must be writable by the web user if anything generates at runtime; in CI you can just read the generated file. Commit or .gitignore it deliberately — don't let it drift between environments.L5_SWAGGER_UI_ASSETS_PATH); stay on a current 10.x/11.x line rather than pinning an old 8.x. v9+ needs PHP ≥ 8.2, Laravel ≥ 11, swagger-php ≥ 5; v10.1 moves to swagger-php 6, which is a breaking change for any custom processors.storage/api-docs/api-docs.json directly and diff it against expectations — it's the ground truth, not the UI. If the JSON looks right but the UI doesn't, it's a view/config/asset problem; if the JSON is wrong, it's scanning/attributes. Running vendor/bin/openapi app -o /tmp/spec.json standalone isolates whether swagger-php or L5-Swagger is at fault.OpenApi\Processors via the config (v11.1 also allows positioning them relative to built-in processors), a custom generator_factory, and overriding the controller/view. Use these to inject global tags, sort operations, or add vendor extensions like x-code-samples without touching every controller.FormRequest rules, so validation and docs diverge silently. Either derive parameter descriptions from the rule array or add a CI test that every form request has a matching documented body — otherwise the "docs" become a lie within two sprints.How can I help you explore Laravel packages today?