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
27 changes: 25 additions & 2 deletions docs/guide/extension-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ code:
without either being written out. The subclass is an `OA\Schema` as far as the rest of the
pipeline is concerned, so merging, containment and compilation treat it the same.

## Contributing to the Specification

`Builder::withSpecification()` hands you the assembled `Specification` before the resolver
runs. Add attributes to it and they go through the rest of the pipeline as scanned ones do:

<<< @/snippets/guide/extension-points/contribution.php

That produces:

<<< @/snippets/guide/extension-points/contribution-3.1.0.yaml

The pipeline reads a reflector, not a source file, so where an attribute came from does not
matter. A contribution that carries one is treated as the assembler's own: an operation given
its `ReflectionMethod` takes summary, description and operation id from the method, and a
parameter given its `ReflectionParameter` takes its name, type and whether it is required. A
router that knows `[UserController::class, 'index']` for a route can hand all of that over.
What has no reflector anywhere, an entity registry or a serializer's configuration, goes in
bare, and this is the only way it gets in.

An augmenter can also `add()` attributes, but it runs after resolution, so a `$ref` inside
what it adds stays unresolved. Use an augmenter to enrich what is there, and this to put
something there.

## Resolvers

Seeding from reflectors means the specification can name a class that was never a source: a
Expand Down Expand Up @@ -126,8 +149,8 @@ alternative.
makes the DTOs worth having. Metadata that only means something to one integration belongs
in an `Attachable`, not in a widened `$ref: string|object`.

**There is no framework-specific code, and no plans for any.** Translators, augmenters and
attachables are the contract; anything a framework needs can be built from them, outside
**There is no framework-specific code, and no plans for any.** Translators, contributions,
augmenters and attachables are the contract; anything a framework needs can be built from them, outside
this repository.

**There are no events or listeners.** The pipeline is deterministic and reads top to bottom,
Expand Down
1 change: 1 addition & 0 deletions docs/guide/modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The modes aim for equivalent output from the same source, but differ in what the
| Processor chain (`withGenerator()`) | Yes | Scanning only (`MergeJsonContent`/`MergeXmlContent`) | No |
| Resolver (`withResolver()`) | No | Yes (`Resolver\Reflection` by default) | Yes (`Resolver\Reflection` by default) |
| Augmenter pipeline (`withAugmenters()`) | No | Yes | Yes |
| Contributions (`withSpecification()`) | No | Yes | Yes |
| Version-aware compilation | No (single serializer) | Yes | Yes |

## Migration path
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/augmenters.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ explicitly set.

Generates operationId for operations that don't have one explicitly set.

The id is derived from method, path and the declaring method, function or class. An
operation with no reflector is identified by method and path alone.

#### Config settings
- **operationIds.hash** : `bool` · default: `true`
If set to <code>true</code> generate ids (md5) instead of clear text operation ids.
Expand Down
47 changes: 25 additions & 22 deletions docs/reference/builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ See the [Processing Modes](/guide/modes) guide for a full comparison and migrati

## API

A `with*()` hook runs either when called, against an object the builder already holds, or
during the build, once its subject exists.

### Sources

```php
Expand Down Expand Up @@ -122,10 +125,27 @@ $builder->withGenerator(function (\OpenApi\Generator $generator) {
```

The callable receives a pre-configured `Generator` instance and may either modify it in-place or return a new instance.
The hook runs during the build, and a second call replaces the first.

### Contributing attributes (spec/hybrid mode) {#contributions}

`withSpecification()` runs after assembly and before resolution, and takes attributes the
assembler did not build, with or without a reflector:

```php
$builder->withSpecification(function (\OpenApi\Specification $specification) {
$specification->add(new \OpenApi\Spec\Operation\Get(path: '/users'));
});
```

Contributions are resolved, augmented and compiled with everything else. Hooks accumulate:
every one runs, in registration order. See
[Contributing to the Specification](/guide/extension-points#contributing-to-the-specification).

### Augmenter configuration (spec/hybrid mode) {#augmenters}

For spec and hybrid modes, use `withAugmenters()` to configure the augmenter pipeline:
For spec and hybrid modes, use `withAugmenters()` to configure the augmenter pipeline. The
hook runs when called; repeated calls configure the same pipeline:

```php
use OpenApi\Augmenter;
Expand Down Expand Up @@ -158,7 +178,8 @@ The resolver handles FQCNs that are referenced by the specification but have no

`Resolver\Reflection` is registered by default: it collects the referenced class with the assembler in use, so adding a single controller is enough to pick up everything it references — no need to list all related classes as sources.

Use `withResolver()` to add your own:
Use `withResolver()` to add your own. The hook runs when called; repeated calls configure the
same instance:

```php
use OpenApi\Resolver;
Expand All @@ -174,7 +195,8 @@ Resolvers implement `OpenApi\Contracts\ResolverInterface` and receive the FQCN a

### Attribute factory configuration (spec mode) {#attribute-factory}

Use `withAttributeFactory()` to add custom attribute translators:
Use `withAttributeFactory()` to add custom attribute translators. The hook runs when called;
repeated calls configure the same instance:

```php
use OpenApi\Utils\AttributeFactory;
Expand Down Expand Up @@ -206,22 +228,3 @@ $result->specification(); // ?Specification — spec/hybrid only, null in classi
$result->openApi(); // ?OA\OpenApi — classic only, null in spec/hybrid
```

## Full example (spec mode)

```php
use OpenApi\Builder;
use OpenApi\Builder\Mode;
use OpenApi\Augmenter;

$result = (new Builder())
->setMode(Mode::SPEC)
->setVersion('3.1.0')
->addSource('src/Api')
->withAugmenters(function (\OpenApi\Utils\Pipeline $pipeline) {
$pipeline->get(Augmenter\Cleanup::class)?->setEnabled(false);
$pipeline->get(Augmenter\OperationIds::class)?->setHash(true);
})
->build();

echo $result->toYaml();
```
27 changes: 27 additions & 0 deletions docs/snippets/guide/extension-points/contribution-3.1.0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
openapi: 3.1.0
info:
title: 'Registered routes'
version: 1.0.0
paths:
/pets:
get:
operationId: a258179ad504a1eacaafc2ef43cbd7ba
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
components:
schemas:
Pet:
type: object
properties:
name:
type: string
age:
type: integer
required:
- name
- age
33 changes: 33 additions & 0 deletions docs/snippets/guide/extension-points/contribution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

namespace OpenApi\Snippets\Guide\ExtensionPoints;

use OpenApi\Builder;
use OpenApi\Builder\Mode;
use OpenApi\Builder\Result;
use OpenApi\Spec as OA;
use OpenApi\Specification;

/**
* Routes registered imperatively — no attribute anywhere, so nothing to scan.
*
* @param array<string, class-string> $routes path => the class documenting the response
*/
function buildFromRoutes(array $routes): Result
{
return (new Builder())
->setMode(Mode::SPEC)
->addSource(new \ReflectionClass(Pet::class))
->withSpecification(function (Specification $specification) use ($routes): void {
$specification->add(new OA\Info(title: 'Registered routes', version: '1.0.0'));

foreach ($routes as $path => $model) {
$specification->add(new OA\Operation\Get(path: $path, responses: [
new OA\Response(response: 200, description: 'OK', content: [
new OA\MediaType\Json(schema: new OA\Schema(ref: $model)),
]),
]));
}
})
->build();
}
11 changes: 7 additions & 4 deletions src/Augmenter/OperationIds.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
/**
* Generates operationId for operations that don't have one explicitly set.
*
* The id is derived from method, path and the declaring method, function or class. An
* operation with no reflector is identified by method and path alone.
*
* @implements PipeInterface<Specification>
*/
class OperationIds implements PipeInterface
Expand Down Expand Up @@ -63,13 +66,13 @@ protected function generateId(OA\Operation $operation): ?string
default => null,
};

if ($source === null) {
return null;
}

$method = strtoupper($operation->method ?? 'GET');
$path = $operation->path ?? '';

if ($source === null) {
return $path === '' ? null : $method . '::' . $path;
}

return $method . '::' . $path . '::' . $source;
}
}
41 changes: 41 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Builder

protected ?AttributeFactory $attributeFactory = null;

/** @var list<callable(Specification): (Specification|void)> */
protected array $specificationHooks = [];

/**
* @param BuilderSource|iterable<BuilderSource> $source
*/
Expand Down Expand Up @@ -123,6 +126,9 @@ public function getResolver(): Resolver
/**
* Configure the resolver via callable.
*
* Runs when called, against the resolver the builder holds; repeated calls configure the
* same instance.
*
* @param callable(Resolver): (Resolver|void) $hook
*/
public function withResolver(callable $hook): static
Expand Down Expand Up @@ -150,6 +156,9 @@ public function getAugmenters(): Utils\Pipeline
/**
* Configure the augmenter pipeline via callable.
*
* Runs when called, against the pipeline the builder holds; repeated calls configure the
* same pipeline.
*
* @param callable(Utils\Pipeline<Specification>): (Utils\Pipeline<Specification>|void) $hook
*/
public function withAugmenters(callable $hook): static
Expand All @@ -167,6 +176,11 @@ public function getAttributeFactory(): AttributeFactory
}

/**
* Configure the attribute factory via callable. Translators are registered here.
*
* Runs when called, against the factory the builder holds; repeated calls configure the
* same instance.
*
* @param callable(AttributeFactory): (AttributeFactory|void) $hook
*/
public function withAttributeFactory(callable $hook): static
Expand All @@ -176,12 +190,35 @@ public function withAttributeFactory(callable $hook): static
return $this;
}

/**
* Contribute to the `Specification` after assembly and before resolution.
*
* The callable receives the assembled `Specification` and adds attributes to it with
* `Specification::add()`. A `$ref` in a contribution resolves as one in a scanned attribute
* does, and every augmenter sees what was contributed. A contribution carrying a reflector
* is treated as the assembler's own; one without is metadata nothing else reaches.
*
* Runs during the build, once the `Specification` exists. Hooks accumulate: every one
* runs, in registration order. Classic mode assembles no `Specification`, so the hooks
* are never called there.
*
* @param callable(Specification): (Specification|void) $hook
*/
public function withSpecification(callable $hook): static
{
$this->specificationHooks[] = $hook;

return $this;
}

/**
* Hook to configure the underlying Generator.
*
* The callable receives a default Generator and may either modify it in-place
* or return a fully configured instance.
*
* Runs during the build, once the Generator exists. A second call replaces the first.
*
* @param callable(Generator): (Generator|void) $hook
*/
public function withGenerator(callable $hook): static
Expand Down Expand Up @@ -260,6 +297,10 @@ protected function doBuildSpec(bool $hybrid = false): Result

$specification = $assembler->getSpecification();

foreach ($this->specificationHooks as $hook) {
$hook($specification);
}

$this->getResolver()->resolve($assembler);

if ($hybrid) {
Expand Down
Loading
Loading