Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,46 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.11.0] — 2026-10-02

### Added

- **`@input [method]` is told which operation it describes.** One controller
method may serve many routes — a generic CRUD controller registered under a
controller key per entity — and the fields depend on the route. The method
was called with nothing, so it could only answer for all routes at once,
which in practice meant describing no fields at all. It now receives an
`OperationContext` (version, Api class, controller key, action key,
controller class and method, HTTP verb, `input`/`output`, the action's
options) through the container: declare a parameter typed `OperationContext`,
or any of `$version`, `$controllerKey`, `$actionKey`, `$httpMethod`. A method
declaring none of them is called exactly as before.
- **A `[method]` may return a JSON Schema object** instead of docblock lines.
It goes into the spec as it is — `minimum`, `maxLength`, `pattern`,
`nullable`, `enum`, nested `items` — merged with the operation's other
`@input` lines (`required` lists united). For POST it is a JSON request body;
for GET each top-level property becomes a query parameter with its schema.
- **`@output [method]`**, with the same context and the same two return forms.
- **`$tags[]` describes a list of scalars**: `@input string $abilities[]`
becomes `{type: array, items: {type: string}}`.
- **Linter: `input.undeclared`** (warning). An action whose method visibly
validates its input — `validate()`, `Validator::make()`, `validator()`, a
`FormRequest` parameter — while its docblock declares no `@input` at all.
Narrowed to validating methods so that actions which take nothing are not
reported.

### Fixed

- Nested notation keeps `required`: `$widgets[].slug` lands in `items.required`,
`$address.city` in the object's `required`, and a required root line in the
body's `required`. They were all dropped.
- A root declared after its children (`$widgets[].slug` before `$widgets`) no
longer wipes them.
- `@output` with nested fields no longer drops the flat siblings: `@output
integer $id` next to `@output string $owner.name` used to leave `id` out of
the schema.
- The linter accepts the `[method]` form on `@output`.

## [5.10.1] — 2026-09-14

### Fixed
Expand Down
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,38 @@ Documentation is generated automatically from PHP docblocks. No YAML or JSON fil
* @input integer $items[].quantity Quantity */
```

A list of scalars takes `[]` with no child: `@input string $tags[]`.

### Fields known only at runtime

When the fields depend on the route — one generic controller registered under a
key per entity — `@input [method]` asks a controller method, and the method is
told which operation is being described:

```php
use Dskripchenko\LaravelApi\Services\OpenApi\OperationContext;

/**
* @input integer $id
* @input [entityFields]
*/
public function update(Request $request) { /* ... */ }

public function entityFields(OperationContext $context): array
{
// $context->controllerKey is `users` on /v1/users/update, `posts` on /v1/posts/update
return [
'type' => 'object',
'properties' => ['email' => ['type' => 'string', 'format' => 'email', 'maxLength' => 255]],
'required' => ['email'],
];
}
```

The method may return docblock lines (`['string $email Email']`) or a JSON
Schema object, which goes into the spec as it is — constraints and all. `@output
[method]` works the same way. See [docs/docblock-tags.md](docs/en/docblock-tags.md#dynamic-inputs-from-a-method).

### Headers, security, responses

```php
Expand Down Expand Up @@ -560,7 +592,9 @@ php artisan api:lint --unrouted # also: public methods no action points at
```

It reads the route map and the docblocks with the same parser the OpenAPI
generator uses. Full list of rules: [docs/linting.md](docs/en/linting.md).
generator uses, and also reports an action that validates its input without
declaring any (`input.undeclared`). Full list of rules:
[docs/linting.md](docs/en/linting.md).

## API Export

Expand Down
37 changes: 36 additions & 1 deletion docs/de/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,39 @@ Die Dokumentation wird automatisch aus PHP-Docblöcken generiert. Keine YAML- od
* @input integer $items[].quantity Quantity */
```

Eine Liste von Skalaren schreibt man mit `[]` ohne Kindfeld: `@input string $tags[]`.

### Felder, die erst zur Laufzeit bekannt sind

Hängen die Felder von der Route ab — ein generischer Controller ist pro Entität
unter einem eigenen Schlüssel registriert —, fragt `@input [method]` eine
Controller-Methode, und der Methode wird mitgeteilt, welche Operation gerade
beschrieben wird:

```php
use Dskripchenko\LaravelApi\Services\OpenApi\OperationContext;

/**
* @input integer $id
* @input [entityFields]
*/
public function update(Request $request) { /* ... */ }

public function entityFields(OperationContext $context): array
{
// $context->controllerKey is `users` on /v1/users/update, `posts` on /v1/posts/update
return [
'type' => 'object',
'properties' => ['email' => ['type' => 'string', 'format' => 'email', 'maxLength' => 255]],
'required' => ['email'],
];
}
```

Die Methode darf Docblock-Zeilen (`['string $email Email']`) oder ein JSON-Schema-Objekt
zurückgeben, das unverändert in die Spezifikation übernommen wird — samt aller
Einschränkungen. `@output [method]` funktioniert genauso. Siehe [docblock-tags.md](docblock-tags.md#dynamische-eingaben-aus-einer-methode).

### Header, Sicherheit, Antworten

```php
Expand Down Expand Up @@ -463,7 +496,9 @@ php artisan api:lint --unrouted # zusätzlich: öffentliche Methoden ohne Aktion
```

Liest Routen-Tabelle und Docblocks mit demselben Parser wie der
OpenAPI-Generator. Vollständige Regelliste: [linting.md](linting.md).
OpenAPI-Generator und meldet außerdem eine Aktion, die ihre Eingaben validiert,
ohne welche zu deklarieren (`input.undeclared`). Vollständige Regelliste:
[linting.md](linting.md).

## API-Export

Expand Down
101 changes: 99 additions & 2 deletions docs/de/docblock-tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,96 @@ Erzeugt:

Erzeugt `$ref: '#/components/schemas/OrderCreateRequest'` im requestBody. Das Modell muss in `getOpenApiTemplates()` definiert sein.

### Dynamische Eingaben aus Methode
### Liste von Skalaren

```php
@input array ?$abilities Abilities
@input string $abilities[] One ability
```

`[]` ohne Kindfeld beschreibt das Element selbst:
`{"type": "array", "items": {"type": "string"}}`.

### Pflichtfelder in verschachtelten Strukturen

Eine Zeile ohne `?` ist auf ihrer eigenen Ebene erforderlich: `$widgets[].slug`
landet in `items.required`, `$address.city` im `required` von `address`, und die
Wurzelzeile (`$widgets`, `$address`) im `required` des Bodys. Eine Wurzel, die nur
ueber ihre Kinder bekannt ist, sagt nichts ueber sich selbst aus und wird nicht als
erforderlich markiert.

### Dynamische Eingaben aus einer Methode

```php
@input [getOpenApiMetaInputs]
```

Ruft die Methode auf dem Controller auf und fuegt die zurueckgegebenen Eingaben zusammen.
Ruft die Methode auf dem Controller auf und fuegt das Ergebnis in die Eingaben der
Operation ein. Gedacht fuer Felder, die erst zur Laufzeit bekannt sind — gebaut
aus einem Modell, einer Konfiguration oder der Route selbst.

**Der Operationskontext.** Eine Controller-Methode kann viele Routen bedienen: ein
generischer CRUD-Controller wird pro Entitaet unter einem eigenen Controller-Schluessel
registriert, und die Felder haengen von der Entitaet ab. Deshalb wird der Methode
mitgeteilt, welche Operation beschrieben wird. Sie erhaelt ueber den Container das,
was sie davon deklariert:

| Parameter | Wert |
|---|---|
| `OperationContext $any` (nach Typ) | der gesamte Kontext, siehe unten |
| `string $version` | die API-Version, `v1` |
| `string $controllerKey` | der Controller-Schluessel aus `getMethods()` |
| `string $actionKey` | der Aktions-Schluessel aus `getMethods()` |
| `string $httpMethod` | das HTTP-Verb, kleingeschrieben |

`Dskripchenko\LaravelApi\Services\OpenApi\OperationContext` enthaelt
`version`, `apiClass` (die `BaseApi`-Unterklasse), `controllerKey`, `actionKey`,
`controllerClass`, `controllerMethod`, `httpMethod`, `tag` (`input` oder
`output`) und `actionOptions` (die Definition der Aktion aus `getMethods()`, so
wie sie geschrieben ist), dazu `operationId()`.

Eine Methode, die nichts davon deklariert, wird genau wie bisher aufgerufen;
bestehende `[method]`-Implementierungen funktionieren also unveraendert weiter.

**Was die Methode zurueckgibt.** Entweder:

- eine Liste von Docblock-Zeilen, die historische Form —
`['string $name Name', 'integer ?$age Age']`;
- ein JSON-Schema-**Objekt** — ein Array mit `properties` oder `type: object`. Es
wird unveraendert in die Spezifikation uebernommen, daher steht alles zur
Verfuegung, was die Zeilensyntax nicht ausdruecken kann: `minimum`, `maxLength`,
`pattern`, `nullable`, `enum`, verschachtelte `items` und so weiter.

Ein zurueckgegebenes Schema wird mit den uebrigen `@input`-Zeilen der Operation
zusammengefuehrt — zuerst deren Properties, danach die des Schemas, `required`-Listen
werden vereinigt. Bei POST entsteht ein JSON-Request-Body; bei GET wird jede
Property der obersten Ebene zu einem Query-Parameter mit eigenem Schema.

```php
use Dskripchenko\LaravelApi\Services\OpenApi\OperationContext;

/**
* Update an entity
*
* @input integer $id Identifier
* @input [entityFields]
*/
public function update(Request $request): JsonResponse { /* ... */ }

public function entityFields(OperationContext $context): array
{
$entity = Entities::find($context->controllerKey);

return [
'type' => 'object',
'properties' => $entity->jsonSchemaProperties(),
'required' => $context->actionKey === 'create' ? $entity->requiredFields() : [],
];
}
```

Die Spezifikation zeigt nun `/v1/users/update` mit den Feldern des Users und
`/v1/posts/update` mit denen des Posts — aus einer einzigen Methode.

---

Expand Down Expand Up @@ -158,6 +241,20 @@ Variablennamen werden mit `?` vorangestellt, um ein Antwortfeld als optional zu

---

### Dynamische Ausgaben aus einer Methode

```php
@output integer $id Identifier
@output [entityOutput]
```

Dasselbe wie `@input [method]`, nur ist `tag` im Kontext `output`: die Methode
liefert Zeilen oder ein Objektschema, das ueber die uebrigen `@output`-Zeilen gelegt
wird. Eine `@output {Template}`-Zeile hat weiterhin Vorrang, und `@response`-Tags
ersetzen `@output`, sofern vorhanden, vollstaendig.

---

## @header — Anfrage-Header

Definiert Header-Parameter fuer die Operation.
Expand Down
24 changes: 22 additions & 2 deletions docs/de/linting.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ Gruppe zu unterscheiden, geht nicht ohne die ganze Anwendung hochzufahren.
|---|---|---|
| `tag.malformed` | Fehler | Der Tag-Inhalt lässt sich nicht parsen, und der Generator verwirft ihn kommentarlos |
| `tag.empty` | Warnung | Ein Tag, hinter dem nichts steht |
| `tag.callable-misplaced` | Warnung | Die `[method]`-Form an einem anderen Tag als `@input` |
| `tag.callable-misplaced` | Warnung | Die `[method]`-Form an einem anderen Tag als `@input` und `@output` |
| `tag.template-misplaced` | Warnung | Die `{Template}`-Form an einem anderen Tag als `@output` |
| `tag.unknown-template` | Fehler | `@input @Model` / `@output @Model[]` nennt ein nicht definiertes Template |
| `tag.callable-missing` | Fehler | `@input [method]` nennt eine Methode, die der Controller nicht hat |
| `tag.callable-missing` | Fehler | `@input [method]` / `@output [method]` nennt eine Methode, die der Controller nicht hat |
| `tag.unknown-type` | Warnung | Ein Typ außerhalb der bekannten Menge — er wird stillschweigend zu `string` |
| `tag.duplicate-variable` | Warnung | Dieselbe Variable zweimal deklariert; die letzte gewinnt |
| `tag.orphan-nesting` | Warnung | `$address.city` ohne deklariertes `$address` |
Expand All @@ -97,6 +97,26 @@ Gruppe zu unterscheiden, geht nicht ohne die ganze Anwendung hochzufahren.
Von Middleware beigesteuerte Eingaben werden berücksichtigt: ein `@default` für
eine dort deklarierte Variable ist legitim und wird nicht gemeldet.

### Fehlende Auszeichnung

| Regel | Schwere | Bedeutung |
|---|---|---|
| `input.undeclared` | Warnung | Die Methode validiert ihre Eingaben, und ihr Docblock hat überhaupt kein `@input` |

Alles oben prüft die vorhandene Auszeichnung; diese Regel betrifft die fehlende.
„Kein `@input`“ allein wäre Rauschen — viele Aktionen nehmen nichts entgegen —,
deshalb schlägt die Regel nur dort an, wo die Methode sichtbar etwas validiert:
ein Aufruf von `validate()` / `validateWithBag()`, `Validator::make()`, der
Helper `validator()` oder ein `FormRequest`-Parameter. Ob die Regeln als Literal
dastehen oder zur Laufzeit zusammengesetzt werden, spielt keine Rolle: so oder so
hat der Endpunkt Felder, und die Spezifikation beschreibt keines davon. Kommentare
werden ignoriert, und es zählen nur die eigenen Tags der Methode — ein von
Middleware beigesteuertes `@input` beschreibt die Eingaben der Middleware, nicht
die Felder, die die Methode prüft.

Sind die Felder erst zur Laufzeit bekannt, deklarieren Sie sie mit
[`@input [method]`](docblock-tags.md#dynamische-eingaben-aus-einer-methode).

## Den Bericht lesen

```
Expand Down
Loading
Loading