✨ Why g4t/swagger
| 🪄 Nothing to annotate | Routes, HTTP methods, middleware, path parameters and controller names become the document. Your code is the only source of truth, so the docs never drift. |
| 📝 Request bodies from FormRequests | Validation rules become typed schemas: enums, files, nested objects, items.*.field arrays, min / max and required fields. |
| 📦 Responses from your code | response()->json([...]), Resources, collections, paginators, abort(404, '…'), returned models, and even the repository or helper a controller calls. |
| 🧬 Real schemas | Resources and models become $ref components, so client generators (openapi-generator, orval, Kiota…) produce typed models. |
| 🧪 Try it out, for real | Typed forms, file uploads, environments with {{variables}}, several logins, local validation, error cases, history and code in five languages. |
| 🎛️ A settings dashboard | Change any option, drag endpoints into order, hide or deprecate them, write English / Arabic texts and create secret partner links, all from the browser. |
| 🔒 Safe by default | Off in production unless you enable it, optional Basic Auth, a cache file that is never public, and partner links stored only as hashes. |
| 🌍 English and العربية | Readers switch the language next to the title; Arabic text reads right to left. |
flowchart LR
A[routes/api.php] --> G((g4t/swagger))
B[FormRequests] --> G
C[Controllers, repositories, helpers] --> G
D[API Resources and models] --> G
G --> O[OpenAPI 3 document]
O --> U1[g4t UI]
O --> U2[Swagger UI]
O --> U3[Scalar]
O --> E[Postman / Insomnia / client generators]
📚 Contents
- Requirements
- Install in 30 seconds
- A five-minute tour
- Describing your API
- What is read from your code · Requests · Responses · Repositories and helpers · Your own examples
- Extensions
- The documentation UI · Finding things · Trying requests · Sharing and code · Appearance
- Logging in from the docs
- Security schemes, headers and environments
- Settings dashboard · Endpoints · Partner links · Where settings are saved
- Configuration reference
- Going to production
- Artisan commands
- When something is off
- Upgrading to 5.0
- Testing, contributing and license
🧰 Requirements
| Laravel | PHP |
|---|---|
| 13 | 8.3 – 8.5 |
| 12 | 8.2 – 8.5 |
| 11 | 8.2 – 8.4 |
| 10 | 8.1 – 8.3 |
| 9 | 8.0 – 8.2 |
Enum features (Rule::enum(), enum casts in response schemas) need PHP 8.1, like enums themselves.
🚀 Install in 30 seconds
composer require g4t/swagger
php artisan vendor:publish --tag=swagger-config
Open /swagger/documentation, for example https://your-app.test/swagger/documentation. That is it: every route in routes/api.php is already documented.
[!NOTE] Only routes in the
apimiddleware group are documented. SetSWAGGER_INCLUDE_WEB_ROUTES=trueto include web routes that use your own controllers.
[!TIP] The docs are off in production by default. Read Going to production before you deploy.
Video walkthrough
🧭 A five-minute tour
Write your API the way you already do:
// routes/api.php
Route::apiResource('products', ProductController::class);
Route::post('auth/login', [AuthController::class, 'login'])->summary('Log in');
// app/Http/Requests/StoreProductRequest.php
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'price' => ['required', 'numeric', 'min:0'],
'status' => ['required', Rule::enum(ProductStatus::class)],
'tags' => ['nullable', 'array'],
'tags.*' => ['string'],
'image' => 'required|image|mimes:jpg,png|max:2048',
];
}
// app/Http/Controllers/ProductController.php
public function store(StoreProductRequest $request): ProductResource
{
return new ProductResource(Product::create($request->validated()));
}
public function show(Product $product): ProductResource
{
abort_if($product->isArchived(), 404, 'Product not found.');
return new ProductResource($product->load('category'));
}
Without a single annotation, the documentation now shows:
| Endpoint | What you get |
|---|---|
POST /api/products |
A multipart body with a typed status enum, a tags array and an image file field · a 201 response whose schema is #/components/schemas/Product, with category as a $ref to Category · a 422 validation example |
GET /api/products/{product} |
An integer path parameter · a 200 example · a 404 example with "Product not found." |
POST /api/auth/login |
The summary "Log in" from the route |
🏷️ Describing your API
Most text is derived for you. When you want more, add it to the route:
Route::get('users', [UserController::class, 'index'])
->summary('List users') // short title
->description('Paginated. Filter with `?search=`.'); // Markdown allowed
Route::get('internal/health', HealthController::class)->hiddenDoc(); // not in the public docs
Route::get('orders/{order}/invoice', [OrderController::class, 'invoice'])
->deprecated('2026-12-31', 'GET /api/v2/orders/{order}/invoice'); // removal date, replacement
Route::post('payments/split', [PaymentController::class, 'split'])->beta();
use G4T\Swagger\Attributes\SwaggerSection;
#[SwaggerSection('Everything about your users')] // description of the controller's section
class UserController extends Controller
{
}
Without ->summary(), a readable summary comes from the action name:
| Action | Summary |
|---|---|
index |
List users |
store |
Create user |
show |
Get user |
update |
Update user |
destroy |
Delete user |
updateStatus |
Update status |
The dashboard can also rewrite every summary and description, in English and Arabic, without touching the code.
🔍 What is read from your code
Requests
| Your code | In the document |
|---|---|
FormRequest on a POST / PUT / PATCH action |
Request body (application/json and multipart/form-data) |
FormRequest on a GET / DELETE action |
Query parameters |
required, nullable, sometimes, required_if … |
required list (conditional rules are not required) |
string, integer, numeric, boolean, array, date, email, uuid |
Types and formats |
min, max, size, between |
minLength / maxLength or minimum / maximum |
in:…, Rule::in(), Rule::enum() |
enum values |
file, image, mimes, mimetypes |
Binary fields (multipart only) |
address.city, items.*.id |
Nested objects and arrays of objects |
confirmed |
An extra *_confirmation field |
{id}, {id?}, whereNumber(), bound model |
Path parameters (optional ones become a second path) |
auth, auth:*, swagger.auth_middlewares |
security on the operation and a 401 response |
Responses
| Your code | In the document |
|---|---|
response()->json([...]), new JsonResponse([...]) |
Example with typed placeholders for variables |
new XResource(...), XResource::make(...) |
$ref to the resource's component |
XResource::collection(...) |
Array of $ref; paginate / simplePaginate / cursorPaginate add data / links / meta |
return $model, Model::findOrFail(), Model::all(), : Model return type |
The model's component, typed from $casts, without $hidden |
abort(404, '…'), abort_if, abort_unless, throw new HttpException |
Error examples per status |
response()->json([...], 409) |
Error example (several per status become named examples) |
store returning a resource, json(..., 201) |
201 Created; noContent() gives 204 |
$this->whenLoaded('rel'), ->load(), ->with() |
Relations are included only when they are loaded |
Repositories and helpers
A controller that hands the work to a repository or a response helper is followed into it, up to three calls deep, with the called method's parameters set from the arguments. Nothing is executed: the source is read.
// Controller
public function store(CreateTicketRequest $request)
{
return $this->ticketRepository->store($request); // typed with an interface? the class bound in the container is used
}
// Repository
if (! $order) {
return Helper::responseError('Order not found', code: Response::HTTP_NOT_FOUND);
}
return Helper::responseSuccess('Ticket created');
// Helper
public static function responseSuccess($message, $data = null)
{
return response()->json(['status' => true, 'message' => $message, 'data' => $data], Response::HTTP_OK);
}
The document gets a 200 example {"status": true, "message": "Ticket created", "data": null} and a 404 example with "Order not found". Each method is read once per generation, so following calls costs a few milliseconds.
Your own examples
Static analysis has limits. To force an example for a status, add a JSON file under storage/swagger/{Controller}/{uri}/{status}.json, where {Controller} is the controller name without "Controller" and {uri} is the route URI with /, {, } and ? replaced by -.
For GET /api/products/{product} on ProductController, that is storage/swagger/Product/-api-products--product-/200.json:
{
"status_text": "The product",
"response": { "id": 1, "name": "Linen shirt" }
}
🧩 Extensions
Teach the generator about code it cannot read on its own: Spatie Laravel Data, Laravel Actions, JSON:API, your own DTOs or response helpers. An extension is a class with up to four hooks; override only the ones you need.
Response wrappers need no extension. return ApiResponse::success($user), a trait method like $this->successResponse($data) and your own helper functions like api_success($data) are followed, with the status they pass and the resource or model inside.
php artisan make:swagger-extension SpatieData # app/Swagger/SpatieDataExtension.php
use G4T\Swagger\Extensions\{Example, Extension, RequestSpec, ReturnedValue, RouteInfo};
use Spatie\LaravelData\Data;
use Illuminate\Support\Str;
class SpatieDataExtension extends Extension
{
// public function store(UserData $data) → the JSON body
public function request(RouteInfo $route): ?RequestSpec
{
$class = $route->parameterClass(Data::class);
return $class ? RequestSpec::fromClass($class, fn ($name) => Str::snake($name)) : null;
}
// return UserData::from($user) → the example and a reusable UserData schema
public function response(ReturnedValue $value): ?Example
{
return $value->isStaticCallOn(Data::class, 'from')
? Example::fromClass($value->staticCall()['class'], fn ($name) => Str::snake($name))
: null;
}
}
Register it in config/swagger.php, or from a package's service provider:
"extensions" => [App\Swagger\SpatieDataExtension::class],
\G4T\Swagger\Swagger::extend(SpatieDataExtension::class); // in boot()
| Hook | Called with | Returns |
|---|---|---|
request(RouteInfo $route) |
Every documented route, before the FormRequest rules are read | A RequestSpec, or null to let the next extension (then the FormRequest) decide |
response(ReturnedValue $value) |
Every returned expression, and values nested in a json([...]) payload, in controllers and the repositories / helpers they call |
An Example, or null |
operation(array $operation, RouteInfo $route) |
Every OpenAPI operation, once built | The operation, changed or not (content types, extra responses, headers…) |
document(array $document) |
The whole document, at the end | The document |
Building blocks
RequestSpec::fromRules($rules) |
Laravel rules, read exactly like a FormRequest: types, enums, files, nested fields |
RequestSpec::fromClass($class, $key) |
A JSON body from a class's public typed properties; $key maps names (e.g. snake case) |
RequestSpec::fromSchema($schema) |
An OpenAPI schema, as is |
->asQuery() |
The rules describe query parameters, even on POST |
Example::of($value, $component) |
A literal example; with a component name it becomes a $ref schema |
Example::fromClass($class, $key) / Example::listOf(...) |
An example from typed properties; the class and nested classes become components |
$example->wrap(fn ($v) => ['data' => $v]) |
An envelope around it |
RouteInfo |
route, method, uri, controller, action, operationId, tag, reflection(), parameterClass($class), hasMiddleware($name) |
ReturnedValue |
source, class, method, staticCall(), newObject(), isStaticCallOn($class, $method), resolveClass($name) |
fromClass() types array and collection properties from a @var list<X> / X[] doc comment (or @param on a promoted constructor parameter), and reads enums and dates.
[!NOTE] Nothing is executed and an extension cannot break the docs: an exception in a hook is logged (with
APP_DEBUG) and ignored. Without extensions there is no extra cost.
🖥️ The documentation UI
Three interfaces ship with the package. Pick one with ui, or with ?ui= on the URL for one visit:
ui |
Interface |
|---|---|
g4t (default) |
The package's own reference UI, bundled, nothing loaded from a CDN |
swagger |
Swagger UI, bundled, with a theme picker |
scalar |
Scalar API Reference, loaded from jsDelivr at a pinned version |
Everything below is in the g4t UI.
Finding things
| Feature | What it does |
|---|---|
| 🔎 Search everything | The filter (/) matches paths and summaries, and also request and response fields, descriptions and examples: type phone to find every endpoint that sends or returns it. |
| ⌨️ Command palette | ⌘K / Ctrl+K jumps to any endpoint, field or section, or runs an action: log in, switch environment, export, change mode or language, open the models diagram. |
| 🧷 Where is this field used | Click a field name in any table to list every endpoint that has it, and where (query, request or response). |
| 🕸️ Models diagram | The response models and the $ref links between them; click one to see what it links to and the endpoints that use it. |
| 🧭 Getting started | A card above the endpoints with the base URL, how to authenticate, the headers to send, the shape of 401 / 403 / 404 / 422 errors, pagination and deprecations, all worked out from the document. |
| 🗂️ Navigation | One section per controller, a version switcher, deep links (#op-<operationId>), and a drawer on phones. |
Trying requests
| Feature | What it does |
|---|---|
| 🧪 Try it out | A typed form (selects for enums and booleans, date and file pickers, one row per array item) or a JSON editor, with real requests, status, timing and highlighted responses. |
| ✅ Checked before sending | Values are compared with the documented rules (required, types, lengths, enums, emails) and the problems are listed; Send anyway sends them as they are. |
| 💥 Error cases | One click sends invalid requests on purpose: no credentials, an unknown id, a missing, too long or invalid field. A table shows which got the expected 401 / 404 / 422. |
| 🌐 Environments | Base URL, headers and {{variables}} usable in any parameter, field, header or body; {{token}} is the current bearer token. |
| 🕘 Memory and history | Inputs survive reloads (Reset per endpoint); the last 10 requests per endpoint can be restored or re-run. |
| ⏱️ Response times | Each endpoint shows the average time of your last requests, and a card lists the slowest first. Kept in your browser only. |
| 📄 Responses | Status-code tabs, named examples, and an Example / Schema switch. |
Sharing and code
| Feature | What it does |
|---|---|
| 💻 Code samples | cURL, JavaScript (fetch), Dart (Dio) and PHP (Laravel Http) for every request. |
| 📱 Flutter | Request and response model classes with fromJson / toJson, and a repository method using Dio, ready to paste into an app. |
| 🔗 Share links and QR codes | A link that opens Try it out with the same values, for a teammate to press Execute, plus a QR code to open it on a phone. Credentials, and values whose name looks like one (password, token, api_key…), are never put in it. |
| 📤 Export | The OpenAPI document as JSON or YAML, a Postman collection (a folder per section, path variables, {{baseUrl}} and {{token}}) and environment, or an Insomnia export with every environment. |
| 🎤 Present mode | One endpoint full screen in large type; ← / → moves through them, for walking a team through the API. |
| 🌍 English / العربية | Texts written in the dashboard in both languages; readers switch next to the title. |
Appearance
Light (default), dark or system mode, comfortable or compact density, and accent colors.
// config/swagger.php
"ui" => env("SWAGGER_UI", "g4t"), // g4t | swagger | scalar
"g4t" => [
"mode" => "light", // light | dark | system
"density" => "comfortable", // comfortable | compact
"getting_started" => true, // the Getting started card
],
"theme" => env("SWAGGER_THEME", "default"), // Swagger UI theme / g4t accent
"scalar" => [
"version" => "1.73.0",
"theme" => "purple", // default, moon, purple, solarized, bluePlanet, deepSpace, saturn, kepler, mars, laserwave, ...
"layout" => "modern", // modern | classic
"dark_mode" => null, // null lets the reader toggle
"locale" => null, // e.g. "ar"
"direction" => "auto", // auto | ltr | rtl
],
The Swagger UI files are published with the package assets (php artisan vendor:publish --tag=swagger-assets). Each entry of versions becomes a definition in Swagger UI and a source in Scalar.
🔑 Logging in from the docs
Point auto_login at your login endpoint and a Log in button appears in every interface. It posts the credentials as JSON, reads the token from the response, and sends it as Authorization: Bearer … with every protected operation. Readers can also paste a token by hand (Authorize), and the token is shared between the three UIs until Log out.
"auto_login" => [
"url" => env("SWAGGER_LOGIN_URL"), // e.g. "/api/auth/login"; null hides the login form
"method" => "POST",
"fields" => ["email" => "Email", "password" => "Password"],
"token_path" => env("SWAGGER_LOGIN_TOKEN_PATH"), // dot path; token, access_token, data.token… are tried when empty
"security_scheme" => "bearerAuth", // key in security_schemes
"prefill" => [], // pre-filled values, only rendered in the local environment
],
Several logins. A customer API and an admin API? Make auto_login a list. The g4t Log in dialog lets the reader pick one, and each fills its own security_scheme, so a reader can be signed in to all of them at once. Swagger UI and Scalar use the first one.
"auto_login" => [
["name" => "Customer", "url" => "/api/v1/auth/login", "security_scheme" => "bearerAuth"],
["name" => "Admin", "url" => "/api/admin/login", "fields" => ["username" => "Username", "password" => "Password"], "security_scheme" => "adminAuth"],
],
🛡️ Security schemes, headers and environments
Every scheme in security_schemes is documented and works in Try it out: Authorize shows one field per scheme, and each request carries the credential where its scheme expects it. Protected routes accept any one of the schemes.
"security_schemes" => [
"bearerAuth" => ["type" => "http", "scheme" => "bearer"], // Authorization: Bearer …
"apiKey" => ["type" => "apiKey", "in" => "header", "name" => "X-API-Key"], // X-API-Key: …
"queryKey" => ["type" => "apiKey", "in" => "query", "name" => "api_key"], // ?api_key=…
"basicAuth" => ["type" => "http", "scheme" => "basic"], // Authorization: Basic …
],
API keys in a cookie are documented, but a browser page cannot set them; use the code samples for those.
A header on every request (a language, a tenant, a request id…) goes in global_headers; every UI then shows an input for it:
"global_headers" => [
["name" => "Accept-Language", "description" => "Response language", "enum" => ["en", "ar"], "default" => "en"],
["name" => "X-Tenant", "required" => true, "example" => "acme"],
"X-Request-Id",
],
Shared environments switch the base URL, the {{variables}} readers type in any field, and headers sent with every request. Readers can create their own; the ones in environments are shared with everyone and read-only for readers, who can duplicate them to change values.
"environments" => [
[
"name" => "Staging",
"base_url" => "https://staging.example.com",
"variables" => ["tenant" => "acme"],
"headers" => ["X-Tenant" => "{{tenant}}"],
],
],
[!WARNING] Shared environments are visible in the page. Never put secrets in them.
🎛️ Settings dashboard
Everything in this README can also be changed from a dashboard, without editing config/swagger.php. It is off until it has credentials:
SWAGGER_DASHBOARD_USERNAME=admin
SWAGGER_DASHBOARD_PASSWORD=a-long-random-password # or a bcrypt/argon hash from Hash::make()
Then open /swagger/dashboard (SWAGGER_DASHBOARD_URL). Sign-in uses the session (the web middleware group) and allows 5 attempts a minute per IP.
| Section | What you change |
|---|---|
| General | Title, version, contact, description, and the Arabic title and description |
| Interface | g4t / Swagger UI / Scalar, mode, density, accent, the Getting started card |
| Endpoints | Order, visibility, texts, status and notes (below) |
| Versions and servers | Version filters (v1, or path segments like driver/v1) and servers |
| Security schemes | Bearer, basic, API keys, and the middleware that marks a route as protected |
| Global headers | Headers documented on every operation |
| Environments | Shared environments with variables and headers |
| Try it out logins | One or several logins, each with its fields and scheme |
| Generation | Response examples, error examples, schemas, web routes, prefixes, Spatie Query Builder |
| Access | Docs on or off, Basic Auth and its credentials |
| Partner links | Secret links for partners (below) |
Endpoints: order, visibility, texts and status
- Drag sections and endpoints to set their order in every UI.
- Hide an endpoint, or a whole controller with the eye next to its name (endpoints added to it later stay hidden too). The routes keep working.
- Texts: the pen button edits the summary and description in English (replacing the text from your code) and in Arabic.
- Status: New, Beta, or Deprecated with a removal date and a replacement, plus a note for clients. The g4t UI shows a badge, a notice with the days left, and a Deprecations list in Getting started; Swagger UI and Scalar strike it through (
deprecated: true) and show the same text in the description. A status set in code with->deprecated()or->beta()is replaced by the dashboard; choose Stable to clear it.
Partner links
A secret address per partner or client, for when you want someone to see part of your API and nothing else.
- Opens the g4t UI without the docs password, even when the docs are turned off with
SWAGGER_ENABLED=false. - Shows only what you pick: whole sections, single endpoints found with a search, or both, and only the schemas they use.
- A single endpoint is shown even when it is hidden from the public docs (with the dashboard eye or
->hiddenDoc()), so you can share an internal endpoint with one client only. - Versions, shared environments and security schemes: Default (all of them), or only the ones you pick. Environments can also be None, which keeps their base URLs and values private.
- A link limited to some versions never opens another one, even with
?version=in the address. - With picked schemes, protected endpoints ask for those schemes, and only their Try it out logins are shown.
- A link limited to some versions never opens another one, even with
- Can expire on a date. Removing the link revokes it.
- The address always shows in the dashboard. New address makes another one, and the old one stops working when you save, for example when it leaked.
- Requests are matched on a sha256 of the token. The token itself is kept encrypted with your
APP_KEYso the dashboard can show it. If the key changes, the links still work but their addresses can't be shown, so make new ones.
[!NOTE] Middleware you add in
swagger.middlewarestill applies to partner links.
Where settings are saved
Saved changes go to swagger-settings.json in the project root (SWAGGER_DASHBOARD_SETTINGS_PATH, relative to the root or absolute), outside public/, so it is never served.
- Only the values you changed are kept; everything else keeps following
config/swagger.php. Saving the config file's value again removes the override. - Reset removes every dashboard change, including partner links.
- Saving also clears the
swagger:cachefile. - Commit the file to ship the same settings to every server. It holds the docs password if you set one in the dashboard.
- Settings saved by earlier versions in
storage/app/swagger/settings.jsonkeep working and move to the new file on the next save. - Turn the dashboard off with
SWAGGER_DASHBOARD_ENABLED=false.
Several servers, Kubernetes or autoscaling
A local file only lives on one server, and a pod loses it when it restarts. Keep the settings on a shared filesystem disk instead, such as s3, so every server reads the same settings and dashboard changes need no deploy:
SWAGGER_DASHBOARD_SETTINGS_DISK=s3
Or choose the disk on the dashboard's Storage page. It copies the saved settings to the disk, checks that they read back, and leaves a small local swagger-settings.json that only says which disk to read. Commit that file (or set the variable above) so new servers find the disk too. A disk set in .env or the config file can't be changed from the dashboard.
- The settings are read on every request, so a disk is read through the cache for
SWAGGER_DASHBOARD_SETTINGS_CACHE_TTLseconds (60 by default;0reads the disk each time). A save updates the cache at once. With a shared store (Redis, database) every server sees the change immediately; with a local one, within that time.SWAGGER_DASHBOARD_SETTINGS_CACHE_STOREpicks the store. SWAGGER_DASHBOARD_SETTINGS_PATHis then the file's path on the disk.- A disk that can't be reached never breaks your app: the config file applies until it's back.
- S3 needs Laravel's adapter:
composer require league/flysystem-aws-s3-v3 "^3.0".
⚙️ Configuration reference
All keys live in config/swagger.php.
| Key | Env | Default | |
|---|---|---|---|
enable |
SWAGGER_ENABLED |
true except in production |
Turns the docs routes on or off |
url |
SWAGGER_URL |
swagger/documentation |
Docs URL; the JSON is at {url}/json |
title / description / version |
SWAGGER_TITLE … |
info block of the document |
|
servers |
APP_URL |
Servers listed in the document | |
versions / default |
["all"] / all |
Version filter: v1 matches /api/v1/…, driver/v1 matches /api/driver/v1/… |
|
show_prefix |
[] |
Only document routes with these prefixes | |
include_web_routes |
SWAGGER_INCLUDE_WEB_ROUTES |
false |
Also document web routes using your controllers |
auth_middlewares |
auth, auth:api, auth:sanctum |
Middleware names that mark a route as protected | |
security_schemes |
HTTP bearer | Bearer, basic, API key in a header or query | |
global_headers |
[] |
Header parameters added to every operation | |
environments |
[] |
Shared environments for the g4t UI | |
auto_login |
SWAGGER_LOGIN_URL, SWAGGER_LOGIN_TOKEN_PATH |
off | One login or a list of logins |
infer_response_examples |
true |
Success examples from controllers | |
infer_error_response_examples |
true |
Error examples (abort, json(…, 4xx), 422) |
|
response_schemas |
SWAGGER_RESPONSE_SCHEMAS |
true |
Response schemas and $ref components |
unwrap_resource_collection_examples |
true |
Non-paginated collections as a top-level array | |
spatie_query_builder |
false |
Adds filter, sort and include to GET routes |
|
extensions |
[] |
Extension classes | |
ui / g4t / theme / scalar |
SWAGGER_UI, SWAGGER_THEME |
g4t |
The interface and its look |
enable_auth |
SWAGGER_AUTH_ENABLED |
false |
Basic Auth on the docs (SWAGGER_USERNAME, SWAGGER_PASSWORD) |
session_ttl |
SWAGGER_SESSION_TTL |
100000 |
Basic Auth idle timeout in seconds, when the docs run with a session |
middleware |
[] |
Extra middleware for the docs routes, e.g. ["web", "auth"] |
|
use_cached_spec_when_present |
SWAGGER_USE_CACHED_SPEC |
true |
Serve the swagger:cache file when it exists |
cached_spec_location |
SWAGGER_CACHED_SPEC_LOCATION |
storage |
Where swagger:cache writes (storage or public) |
dashboard |
SWAGGER_DASHBOARD_* |
off until credentials are set | enabled, url, username, password, settings_path, settings_disk, settings_cache_ttl, settings_cache_store, middleware |
🌐 Going to production
A short checklist for a public or staging server:
- Turn the docs on where you want them:
SWAGGER_ENABLED=true. - Protect them, with Basic Auth or your own guards:
orSWAGGER_AUTH_ENABLED=true SWAGGER_USERNAME=docs SWAGGER_PASSWORD=a-long-random-password # an empty password never authenticates"middleware" => ["web", "auth"]inconfig/swagger.php. - Cache the document during deployment, so it is not generated on every request:
The file is stored outsidephp artisan swagger:cachepublic/and only served through the docs JSON endpoint, so Basic Auth and your middleware still apply. - Share with partners through partner links instead of handing out the docs password.
- Decide about the dashboard: keep it behind strong credentials, or turn it off with
SWAGGER_DASHBOARD_ENABLED=falseand shipswagger-settings.jsonwith the code.
🛠️ Artisan commands
| Command | What it does |
|---|---|
swagger:cache |
Writes the OpenAPI document to the cache file; --clear removes it |
make:swagger |
Older alias of swagger:cache |
make:swagger-extension {name} |
Creates an extension class in app/Swagger |
swagger:mock-server |
Uploads your document to the G4T mock server (experimental) |
G4T provides an optional command that connects your generated documentation to a hosted mock API run by G4T, so you can explore or demo your API while you build it, without hosting that side yourself.
php artisan swagger:mock-server
Set MOCK_SERVER_APP_ID to always update the same mock app without prompts.
[!CAUTION] This feature is under testing. It may change, pause or evolve without much notice. Do not rely on it for business-critical paths yet.
🩺 When something is off
- It must be in the
apimiddleware group (or setSWAGGER_INCLUDE_WEB_ROUTES=truefor web routes that use your controllers). show_prefixlimits the docs to some prefixes, and the version selector shows one version at a time; pick all.- It may be hidden with
->hiddenDoc()or with the eye in the dashboard. - A cached document is served while the cache file exists: run
php artisan swagger:cacheagain, or--clear.
They are off in production by default. Set SWAGGER_ENABLED=true, and protect them as described in Going to production.
Examples come from reading the source, so very dynamic code (a response built through many conditions, a class picked at runtime) cannot always be followed. Force the example with a JSON file, or turn inference off with infer_response_examples / infer_error_response_examples.
- A
swagger:cachefile is being served: rebuild it or runphp artisan swagger:cache --clear. - A value saved in the dashboard overrides
config/swagger.php; the dashboard marks those sections "saved in dashboard". Save the config value there again, or use Reset. - Run
php artisan config:clearafter changing.envif the config is cached.
The request did not reach the server: the selected server URL is not reachable from the browser, or another origin blocks it with CORS. Pick the server marked "This page" in the server selector, or set servers (or APP_URL) to the address the docs run on.
Set token_path (SWAGGER_LOGIN_TOKEN_PATH) to where your login response keeps the token, e.g. data.token or data.access_token.
Publish the assets: php artisan vendor:publish --tag=swagger-assets.
⬆️ Upgrading to 5.0
- The
g4tUI is the new default. SetSWAGGER_UI=swaggerto keep Swagger UI. - Off in production by default.
enablenow defaults toAPP_ENV !== production; setSWAGGER_ENABLED=trueto keep the docs online there. - The cached spec moved out of
public/.swagger:cache/make:swaggerwrite tostorage/app/swagger/{cached_spec_path}. SetSWAGGER_CACHED_SPEC_LOCATION=publicfor the old location, and remove any oldpublic/doc.json(php artisan swagger:cache --clearremoves both). - Basic Auth reads
SWAGGER_AUTH_ENABLED,SWAGGER_USERNAMEandSWAGGER_PASSWORD; an empty password never authenticates.session_ttlreplaces the misspelledsesson_ttl(still read). - Valid OpenAPI 3.0.
pathsandtagsare at the document root, nested rules become nested schemas, optional path parameters produce one path per variant, andauth:sanctumroutes are detected. - Status codes.
storeactions returning a resource are documented as201,noContent()as204. - Security scheme. The default scheme is HTTP bearer (
bearerAuth): paste just the token, withoutBearer. - Same-name controllers in different namespaces get distinct sections and schemas (
V1/Product,V2/Product).
🤝 Testing, contributing and license
composer test
The suite runs on Orchestra Testbench, and GitHub Actions covers every supported pair above (Laravel 9 to 13, PHP 8.0 to 8.5), including the lowest supported dependencies.
- Ideas: the Canny board.
- Issues and pull requests: welcome on GitHub; see CONTRIBUTING.md.
- Security: report vulnerabilities as described in SECURITY.md.
- License: open source under the MIT license.
- Maintainer: Hussein Alaa.
More reading: website · OpenAPI specification · Laravel documentation
