Core concepts
Extensions
Extensions let you customize project-specific type analysis, OpenAPI output, and TypeScript exports.
If native PHP types and PHPDoc do not provide enough information, add an extension with your own analysis logic.
Add extension classes to the extensions array in your autodoc config. Custom extensions run in registration order before autodoc’s built-ins, so they can override built-in handling for native array functions, is_* checks, and enum static calls. For type-returning hooks, null passes control to the next extension. Returning a Type stops the chain for the current call, class, or object.
Upgrading from v1? See the upgrade guide.
Available extension types
1. Class extension
By default, except for enums and classes implementing DateTimeInterface, JsonSerializable or Stringable, autodoc attempts to read class properties using Reflection API, analyzing their native data types and PHPDoc comments, including the PHPDoc comment above the class. However, in some cases, this behavior does not produce the desired results, and custom logic is required.
To create a custom logic for a class, create a class extending AutoDoc\Extensions\ClassExtension with any of the following methods:
use AutoDoc\Analyzer\PhpClass;
use AutoDoc\DataTypes\{ArrayType, NullType, ObjectType, StringType, Type, UnionType};
use AutoDoc\Extensions\ClassExtension;
class CustomClassExtension extends ClassExtension
{
public function getReturnType(PhpClass $phpClass): ?Type
{
// First, filter the classes targeted by this extension.
if (! is_a($phpClass->className, CustomResponse::class, true)) {
return null;
}
// Return a subtype of `AutoDoc\DataTypes\Type` that matches
// the response body.
return new ObjectType([
'type' => new StringType(['success', 'error', 'warning']),
'data' => $phpClass->resolveType(),
]);
}
public function getPropertyType(PhpClass $phpClass, string $propertyName): ?Type
{
// Only handle the target classes and properties.
if (! is_a($phpClass->className, CustomResponse::class, true)) {
return null;
}
// Return a subtype of `AutoDoc\DataTypes\Type` that matches
// the property type.
if ($propertyName === 'data') {
return new UnionType([
new ArrayType(itemType: new StringType),
new NullType,
]);
}
return null;
}
public function getRequestType(PhpClass $phpClass): ?Type
{
// Only handle the target classes and properties.
if (! is_a($phpClass->className, CustomRequest::class, true)) {
return null;
}
// Return a subtype of `AutoDoc\DataTypes\Type` that matches
// the request body.
return new ObjectType([
'data' => $phpClass->resolveType(),
'token' => new StringType(description: 'API access token'),
]);
}
}
If the getReturnType/getPropertyType/getRequestType method returns null, the next extension is processed. If it returns a Type, further extensions are not processed for current class/object.
The getPropertyType method is only checked when accessing the property directly with -> operator - it will not trigger for each property when the associated object is processed.
getRequestType handles classes that represent request bodies, such as form request objects passed to controller methods.
2. Function call extension
Function call extensions determine the return types of global functions. Extend AutoDoc\Extensions\FuncCallExtension:
use AutoDoc\Analyzer\FuncCallContext;
use AutoDoc\DataTypes\StringType;
use AutoDoc\DataTypes\Type;
use AutoDoc\Extensions\FuncCallExtension;
class CustomFuncCallExtension extends FuncCallExtension
{
public function getReturnType(FuncCallContext $call): ?Type
{
if ($call->functionName === 'generate_invoice_number') {
return new StringType(pattern: '^INV-[0-9]+$');
}
return null;
}
}
$call->functionName contains the function name, or null for dynamic calls such as $fn(). Resolved argument types are available in $call->argTypes; see the call context reference.
3. Method call extension
Method call extensions handle instance methods, including nullsafe (?->) calls. They can provide return types and record side effects:
use AutoDoc\Analyzer\MethodCallContext;
use AutoDoc\DataTypes\Type;
use AutoDoc\Extensions\MethodCallExtension;
class CustomMethodCallExtension extends MethodCallExtension
{
public function getReturnType(MethodCallContext $call): ?Type
{
if ($call->methodName === 'refresh') {
// The type of the object the method is called on,
// lazy-resolved and cached:
return $call->getVarType();
}
return null;
}
public function handleSideEffect(MethodCallContext $call): void
{
// Record a request body for the analyzed route:
if ($call->methodName === 'validateInput') {
$call->setRequestType($call->argTypes->get(0)->unwrapType($call->scope->config));
}
}
}
4. Static call extension
Static call extensions handle static methods. The context includes the resolved class name:
use AutoDoc\Analyzer\StaticCallContext;
use AutoDoc\DataTypes\BoolType;
use AutoDoc\DataTypes\Type;
use AutoDoc\Extensions\StaticCallExtension;
class CustomStaticCallExtension extends StaticCallExtension
{
public function getReturnType(StaticCallContext $call): ?Type
{
if ($call->className === Feature::class && $call->methodName === 'enabled') {
return new BoolType;
}
return null;
}
}
5. Throw extension
Throw extensions add exception responses to the generated documentation. Use them when an exception renders a structured error response:
use AutoDoc\Analyzer\ThrowContext;
use AutoDoc\DataTypes\Type;
use AutoDoc\Extensions\ThrowExtension;
class NotFoundExceptionExtension extends ThrowExtension
{
public function getReturnType(ThrowContext $throw): ?Type
{
if ($throw->getThrownClassName() === NotFoundException::class) {
return $throw->scope
->getPhpClass(NotFoundException::class)
->getMethod('render')
->getReturnType();
}
return null;
}
}
getThrownType() resolves and caches the thrown expression on first use. If you only need its class name, getThrownClassName() avoids resolving the full type.
6. Operation extension
Use operation extensions to modify generated OpenAPI operations for some or all routes. For example, add security fields enforced by middleware, which code analysis cannot infer.
To create an operation extension, create a class extending AutoDoc\Extensions\OperationExtension with a handle method:
use AutoDoc\Analyzer\Scope;
use AutoDoc\DataTypes\ObjectType;
use AutoDoc\DataTypes\StringType;
use AutoDoc\Extensions\OperationExtension;
use AutoDoc\OpenApi\MediaType;
use AutoDoc\OpenApi\Operation;
use AutoDoc\OpenApi\RequestBody;
use AutoDoc\OpenApi\Response;
use AutoDoc\Route;
class CustomOperationExtension extends OperationExtension
{
public function handle(Operation $operation, Route $route, Scope $scope): ?Operation
{
/**
* An extension that adds `client_id` and `client_secret` parameters to all POST requests.
*/
if (strtoupper($route->method) === 'POST') {
$extraRequestParams = [
'client_id' => new StringType(description: 'Client ID'),
'client_secret' => new StringType(description: 'Client secret'),
];
$extraRequestParams['client_id']->required = true;
$extraRequestParams['client_secret']->required = true;
$mediaType = $operation->requestBody->content['application/json'] ?? null;
if ($mediaType && $mediaType->type instanceof ObjectType) {
foreach ($extraRequestParams as $key => $paramType) {
$mediaType->type->properties[$key] = $paramType;
}
} else {
$operation->requestBody = new RequestBody(
content: [
'application/json' => new MediaType(
type: new ObjectType($extraRequestParams),
config: $scope->config,
),
],
);
}
}
/**
* Add a potential response with HTTP code 418 to all operations
*/
$operation->responses ??= [];
$operation->responses['418'] ??= new Response;
return $operation;
}
}
An Operation can be handled by multiple extensions. If the handle method returns Operation instead of null, the Operation is overriden.
A MediaType carries a Type and the active Config; the OpenAPI schema is derived from them when the document is serialized, so modifying $mediaType->type is enough.
7. TypeScript export extension
To perform custom manipulations on classes before they are exported as TypeScript types, create a class extending AutoDoc\Extensions\TypeScriptExportExtension with a handle method:
use AutoDoc\Analyzer\PhpClass;
use AutoDoc\DataTypes\Type;
use AutoDoc\Extensions\TypeScriptExportExtension;
class CustomTypeScriptExportExtension extends TypeScriptExportExtension
{
public function handle(PhpClass $phpClass, Type $type): ?Type
{
// Do something with `$type` and return it.
return $type;
}
}
A PhpClass can be processed by multiple TypeScriptExportExtension classes. If the handle method returns a Type instead of null, the Type is overriden.
Call context reference
Every call extension hook receives a context object with these fields:
node- the raw PHP-Parser nodescope- the analysis context, including the active config as$call->scope->configargTypes- anArgumentListof resolved argument types:get($index),has($index),count($args)andfindNamedIndex('name')
Each context adds what is specific to its call form:
FuncCallContext-functionName, pre-extracted (nullfor dynamic calls)MethodCallContext-methodNameandgetVarType(), the lazily resolved type of the receiving objectStaticCallContext-className(pre-resolved) andmethodNameThrowContext-getThrownType()andgetThrownClassName()
Side effects
A call can affect the generated schema even when its return value is ignored. For example, validate() defines the request body whether or not its result is assigned. Handle these cases with handleSideEffect(context): void and the context helpers below:
setRequestType(Type)- record a request body for the analyzed route. Bodies recorded by multiple calls are combined into one shape.setVarType(name, Type)/mutateVar(name, attributes)- replace or extend a variable's type.mutateExpression(node, attributes)- likemutateVar, but also accepts property and array-key targets such as$order->customeror$items['first'].
handleSideEffect fires once per call node, before that node's getReturnType, and also for bare call statements whose value is discarded. It never fires while the analyzer only peeks at a return type without "executing" the call.
Condition narrowing
Call extensions can also narrow argument types in conditional branches. Implement narrowTypeFromCondition(context, bool $negated), then call narrowVarType() or narrowExpressionType() to record what the condition proves. Autodoc applies those facts to the appropriate branches and combines them across && and ||. $negated is true when the condition is negated. Built-in extensions use the same mechanism for checks such as is_string().