v1.57.0 - Sargas: types-first DTO I/O + a single code-first OpenAPI generator
Theme: Types-first I/O & a single code-first OpenAPI generator. A typed-DTO request/response convention (
RequestData/ResponseData,CollectionResponse/PaginatedResponse,#[Rule]/#[ResponseStatus]/HasResponseMessage) where types drive both runtime enveloping and the generated spec, plus auto-422,JsonResource/ResourceCollectionauto-normalization, and ascaffold:dtocommand. The OpenAPI generator is consolidated to a single code-firstreflectengine: the legacy docblock-parsingcommentsgenerator is removed, structure is derived from the live route table + types, and a minimal typed attribute surface (#[ApiOperation]/#[QueryParam]/#[ApiRequestBody]/#[ApiResponse]) documents only what types can't express. Minor release (new features + the comment-generator removal + default changes), per the pre-public cadence. Has### Upgrade Notes.
Added
- OpenAPI
reflectgenerator: hand-authored override attributes (Phase 3 Stage 1). Four method-level attributes inGlueful\Routing\Attributes\let you fill the gaps the code-firstreflectgenerator cannot infer from types alone — structure (paths, params, security, request/response schemas) stays inferred; these cover what types cannot express.#[ApiOperation]overrides the derived summary, description, tags, operationId, and addsdeprecated; every field is optional and omitting a field keeps the derived value.#[QueryParam](repeatable) documents arbitrary query parameters (?days=30,?status=active) not expressible as PHP types; parameters are merged path → field-selection →#[QueryParam]and deduped by(name, in)with#[QueryParam]winning over a generated param of the same name (e.g. overriding the auto-generatedfieldsparam).#[ApiRequestBody]documents a request body the generator can't infer from a hydratingRequestDataparam (a handler that must stay manual — polymorphic login, resource store/update — or a non-JSON body) via a two-parameter, type-first API:schema:is a DTO class reflected to a raw object schema for a (usuallyapplication/json) doc-only body, whileinlineSchema:is a raw OpenAPI array for non-JSON bodies only (multipart file uploads,application/octet-stream, etc.). Exactly one of the two must be set (XOR), andinlineSchemawithcontentType: 'application/json'is rejected withInvalidArgumentExceptionat load time (it would recreate the inline-JSON mini-language). When present the attribute still wins overRequestData/#[Validate]inference; when absent that inference is unaffected.#[ApiResponse]gains a newbody:parameter ('binary'|'text'|'object') for non-JSON response bodies a DTO cannot model (file downloads, HTML pages, opaque blobs);body:is honored only whenschemais null — when both are set the DTO class schema wins; an invalid value throwsInvalidArgumentExceptionat load time. All four attributes are read byRouteReflectionDocGeneratorand are fully guarded (generation never throws). Seedocs/OPENAPI_REFLECT.mdfor the complete override surface and DTO-first usage guide. Glueful\Http\Contracts\HasResponseMessage— a returnedResponseData/CollectionResponse/PaginatedResponsemay now supply its own envelopemessageviaresponseMessage()(defaults unchanged —'Success'/'Created successfully'/'Data retrieved successfully'— when not implemented).- Typed response DTOs: a controller method returning a
ResponseDatais automatically enveloped into the standard{success, message, data}response. A new empty marker interfaceGlueful\Http\Contracts\ResponseDatais the only contract required — whenRouter::normalizeResponse()receives aResponseDatareturn value it delegates serialization to the new pureGlueful\Serialization\ResponseDataSerializer::toArray()and wraps the result inResponse::success($data)(200),Response::created($data)(201), or an equivalent envelope for other 2xx codes. A new method attributeGlueful\Routing\Attributes\ResponseStatussets the success status (#[ResponseStatus(201)]→ 201 Created); the attribute constructor rejects non-2xx codes withInvalidArgumentExceptionat load time (fail-loud, not silently discarded).ResponseDataSerializerreflects public typed properties, skips uninitialized ones, and maps values: backed enum →->value; pure enum →->name;DateTimeInterface→ ISO-8601 (format('c')); nestedResponseData→ recursed; arrays → element-wise; a DTO that declares its owntoArray()method bypasses reflection entirely (escape hatch). A cycle/depth guard (MAX_DEPTH = 5) terminates self-referential DTOs withnullrather than looping. Indocumentation.generator='reflect'modeRouteReflectionDocGeneratorinfers the success-response OpenAPI schema from the handler'sResponseDatareturn type (envelope-wrapped DTO schema at the#[ResponseStatus]status, defaulting to 200), providing symmetry between the runtime payload and the generated spec; an explicit#[ApiResponse]at the same status still overrides, and 4xx/error responses still come from#[ApiResponse]alone. v1 boundaries:ResponseDataonly —JsonResource/ResourceCollection(the rich transformation path) keep their own->toResponse()envelope rather than theResponseDataenvelope (and are auto-normalized through it — see the Resource auto-normalization entry below); public typed properties serialized (or a customtoArray()); success response only (error/4xx via#[ApiResponse]); collections and pagination are not first-class (wrap a list in a plain array property). Seedocs/RESPONSE_DTOS.mdfor the usage guide. - Typed request DTOs: a controller parameter implementing
RequestDatais auto-hydrated and validated from the JSON body. A new marker interfaceGlueful\Validation\Contracts\RequestDataand attributeGlueful\Validation\Attributes\Rule(targets: parameter, property) let you express the request contract as a plain PHP class with constructor-promoted properties. When the router (Router::resolveMethodParameters()) encounters a method parameter whose type implementsRequestData, it decodes the JSON body, validates each field against the parameter's#[Rule('required|email|…')]constraints viaRequestDataHydrator→RuleParser→Validator, and passes the constructed, type-coerced DTO directly to the handler. Validation failure throwsValidationException(→ 422); malformed JSON throwsValidationException::forField('body', …)(→ 422). Indocumentation.generator='reflect'mode the OpenAPI request-body schema is inferred automatically from the DTO's constructor parameters and their#[Rule]constraints — no#[Validate]attribute needed. v1 boundaries: JSON body only (no form/multipart fallback); constructor-promoted properties only (non-promoted/non-public properties are not documented); use aRequestDataparameter instead of#[Validate]for the body — in reflect docs aRequestDataparam supersedes#[Validate], but at runtime#[Validate]is enforced by the opt-inValidationMiddlewarebefore parameter resolution, so combining both on one route is unsupported; a non-scalar or un-ruled promoted param can surface aTypeError(500) rather than a 422. Seedocs/REQUEST_DTOS.mdfor the usage guide and v1 sharp edges. - OpenAPI:
reflectgenerator documents response bodies from typed DTO classes via#[ApiResponse]. A new repeatable method attributeGlueful\Routing\Attributes\ApiResponsedeclares a route's responses by status (#[ApiResponse(200, UserData::class)],#[ApiResponse(404, description: 'Not found')]). In reflect mode each instance is reflected into an OpenAPI response object whose body schema is derived from the typed$schemaDTO class by the new pure reflectorGlueful\Support\Documentation\ClassSchemaReflector::toSchema(). The reflector maps a class's public, non-static typed properties to an OpenAPI object schema: scalars (string/int/float/bool), nullable?T,DateTimeInterface→date-time, backed enums → typedenumvalues (pure enums → case names), nested DTO classes (recursive, with a depth/cycle guard so self-referential DTOs terminate), andarrayproperties whose item type is taken from a@var Foo[]/@var array<Foo>docblock when resolvable (otherwiseitems: {}); it is pure and never throws (a missing/uninstantiable class yields{type: object}).#[ApiResponse]options:collection: truewraps the schema as an array, andenvelope: true(default) wraps it in Glueful's success envelope{success, message, data}(matchingResponse::success()); a schema-less attribute produces a description-only response. Attribute responses overlay the generator's default response set keyed by status — an explicit200replaces the minimal default while the auto401/403/429remain — so handlers with no#[ApiResponse]are unchanged. Handler-method reflection is shared with#[Validate]request-body inference and is fully guarded. - OpenAPI:
DocGeneratornow normalises inline path schemas for OpenAPI 3.1.getSwaggerJson()previously ran the 3.1 nullable transform (nullable: true→type: [T, 'null']) only overcomponents.schemas; it now also walks each operation'sparameters[].schema,requestBody.content.*.schema, andresponses.*.content.*.schema, so inline schemas (e.g. a reflected DTO's nullable property) render in the correct 3.1 array form. No-op under 3.0.x; benefits bothreflectandcommentsmodes. - OpenAPI:
reflectgenerator infers JSON request bodies from#[Validate]. In reflect mode, POST/PUT/PATCH operations now gain arequestBodyderived from the#[Validate(...)]attribute on the route's handler method. A new pure mapperGlueful\Support\Documentation\ValidationRuleSchema::toObjectSchema()turns Laravel-style rule strings (field => 'required|email|min:8', or per-field arrays) into an OpenAPI object schema:required/type rules (string/integer/int/numeric/boolean/bool/array), string formats (email/uuid/url→uri/date/datetime+date_format→date-time),in:→enum, andmin:/max:asminLength/maxLength(strings) orminimum/maximum(numbers); unrecognised rules (confirmed,unique:,exists:,nullable,sometimes, …) are ignored and untyped fields default tostring. The example payload reusesExampleDeriver. Only body-bearing verbs are considered —#[Validate]on GET/DELETE/HEAD/OPTIONS (query validation) yields no body — and handler reflection is fully guarded, so closures, invokables, and unresolvable handlers simply produce no request body and never throw. Response bodies are documented separately via#[ApiResponse](above). - OpenAPI: code-first
reflectgenerator. Newdocumentation.generatorswitch (envAPI_DOCS_GENERATOR) selecting'comments'(default, unchanged docblock/JSON-fragment parsing) or'reflect'. In reflect modeOpenApiGeneratorderives the spec'spathsdirectly from the live route table instead of parsing docblocks: per-routesecurityis computed from the route's real middleware via the configuredSecuritySchemeRegistry, path parameters (withwhere()constraints aspattern) and GraphQL-stylefields/expandquery params come from the route definition, required scopes are documented in prose, and rate-limited routes gain a429with the standard rate-limit headers. The Router is loaded the same way asroute:debug(RouteManifest::load()+ attribute scan); if it was built from the compiled route cache (which strips per-route metadata) the manifest is reset and rebuilt so reflection sees freshRouteobjects. The same registry instance is shared between theDocGeneratorand the reflect generator. Resource-route expansion (include_resource_routes) still applies. (Introduced this release as opt-in alongside the comment generator; later in this same releasecommentswas removed andreflectbecame the sole, unconditional generator — see Removed.) - OpenAPI: opt-in pruning of unreferenced default schemas. New
documentation.options.prune_unreferenced_schemas(envAPI_DOCS_PRUNE_UNREFERENCED_SCHEMAS, defaultfalse). When enabled,DocGenerator::getSwaggerJson()drops the built-in default component schemas (LoginRequest,User,Notification, …) that no path or webhook references, so SDK/codegen tools stop emitting dead types. Reachability is computed transitively frompaths/webhooks, so referenced schemas and their dependencies are kept, and schemas contributed by route/extension fragments are always retained. Off by default for backward compatibility. - Typed collection and pagination response DTOs:
CollectionResponseandPaginatedResponse(Glueful\Http\Responses\). Two new final value objects complete the typed-I/O response story for list endpoints. A handler returningCollectionResponseis rendered as Glueful's standard success envelope ({success, message, data: [...]}) at the handler's#[ResponseStatus]status (default 200, honored). A handler returningPaginatedResponse($items, $page, $perPage, $total)is rendered as the flat pagination envelope produced byResponse::paginated()— keyssuccess, message, data, current_page, per_page, total, total_pages, has_next_page, has_previous_page— always at HTTP 200 (#[ResponseStatus]has no effect on aPaginatedResponse; useCollectionResponsefor a non-200 collection status). Items in both wrappers are serialized element-wise viaResponseDataSerializer(ResponseData DTOs → arrays; plain arrays/scalars and non-ResponseData objects pass through).PaginatedResponse's constructor validates its metadata at construction time:page >= 1,perPage >= 1,total >= 0(invalid values throwInvalidArgumentExceptionimmediately, preventing a division-by-zero inResponse::paginated()). - OpenAPI: reflect-mode list-schema inference from
@return CollectionResponse<Item>/@return PaginatedResponse<Item>docblocks.RouteReflectionDocGeneratornow infers OpenAPI item schemas from the generic type parameter in the handler's return docblock. A fully-qualified class name or a same-namespace short name is resolved and reflected viaClassSchemaReflector::toSchema(); when the docblock is absent or the type cannot be resolved (includinguse-aliased names — aliases are not resolved), the items fall back to{type: object}. ACollectionResponsehandler documents an envelope-wrapped{type: array, items: <Item schema>}at the#[ResponseStatus]status; aPaginatedResponsehandler documents a flat paginated schema with all pagination keys in therequiredlist, always at status 200. An explicit#[ApiResponse]at the same status overrides the inferred schema. - OpenAPI: auto-derived 422 response for handlers with a
RequestDataparameter. Indocumentation.generator='reflect'mode, any handler whose parameter list includes aRequestData-typed parameter now automatically documents a422 Unprocessable Entityresponse matching the runtime validation-error body ({success: false, message: string, errors: {<field>: [string]}}). No annotation is required. An explicit#[ApiResponse(422, …)]on the same handler overrides the auto-derived entry. scaffold:dtocommand.php glueful scaffold:dto <Name>generates a request DTO stub (implementingRequestDatawith a sample#[Rule]property) inapp/DTOs/(orsrc/DTOs/in framework-dev mode). Pass--response(-r) to generate a response DTO stub (implementingResponseData) instead. Pass--force(-f) to overwrite an existing file without prompting.- Returning a
JsonResource,ResourceCollection, orPaginatedResourceResponsefrom a controller is now auto-normalized through its own->toResponse()(the manual call is no longer required;#[ResponseStatus]is threaded as the status). Resource bodies are not reflectable, so document Resource-returning endpoints with#[ApiResponse].
Fixed
- Reflect-mode docs no longer emit a vestigial description-only
200response alongside a non-200 inferred success (e.g. a#[ResponseStatus(201)]handler now documents only201). ClassSchemaReflectorschema fidelity. A@var array<…>property whose type sat on its own docblock line no longer leaks a bogusdescription: "*"into the generated schema (the same-line description regex was tightened). A DTO with no public typed properties now reflects to{type: object, properties: {}}(valid OpenAPI) instead of a malformedproperties: []— the honest representation of a generic open object.- OpenAPI generator: documentation config flags are now honored. The
documentation.options.include_extensionsanddocumentation.options.include_routesflags were never read insrc/(dead flags) anddocumentation.sources.include_framework_routeswas applied only on the--forcepath.OpenApiGenerator::processExtensionAndRouteDocs()now reads all three (each defaulting totrue, so existing specs are unchanged):include_extensions=falseskips generating/merging extension fragments,include_routes=falseskips app route fragments, andinclude_framework_routes=falsenow excludes the framework's own route files on both the force and non-force paths. An app can now produce a spec of only its own routes through config alone. - OpenAPI generator: stale-fragment leakage eliminated. The spec's
pathswere assembled by globbing the wholejson-definitions/{routes,extensions}directories, so per-file fragments written by previous runs (including framework routes from an earlier--force) were re-merged on every subsequent run regardless of config. The generator now merges only the fragment files produced by the current run:forceGenerateExtensionDocs()/forceGenerateRouteDocs()return the files they generated, the non-forceCommentsDocGenerator::generateAll()list is split into extension vs route fragments by directory, and the resulting lists (after flag gating) are merged via the new list-based merge methods. generate:openapi --cleannow removes nested fragments.cleanDefinitionDirectories()previously called@rmdir()on theroutes//extensions/subdirectories (which silently fails on non-empty dirs) and onlyunlink-ed*.jsonat depth 0, soroutes/*.jsonandextensions/<ext>/*.jsonsurvived--cleanand were re-merged on the next run. Cleaning is now recursive: every file at any depth is unlinked, then the now-empty subdirectories are removed deepest-first. The operation never throws if a path is already gone and reports an accurate file/directory count.
Changed
- Reference example:
AuthController::refreshToken()now uses typed request and response DTOs (Glueful\DTOs\RefreshTokenDatafor the request,Glueful\DTOs\RefreshedTokenDatafor the response) instead of manual body extraction andResponse::success(...). The enveloped response body is byte-identical to before:datacarriesaccess_token,refresh_token,expires_in,token_type,userin the same declaration order; the envelope message ('Token refreshed successfully') is supplied viaHasResponseMessageand stored in a private property so it is not serialized intodata. A missing or blankrefresh_tokenfield is rejected by#[Rule('required|string')]as a 422 Validation Error — the SAME status as before (the old code threwValidationException::forField('refresh_token', …), already a 422), so there is NO error-path status change. Theloginmethod was intentionally left manual (request is polymorphic — token/api_key shortcut vs credentials — and the response is polymorphic: full session vs 2FA challenge). - Reference example:
ResourceController::destroy()now returns a typed response DTO (Glueful\Controllers\DTOs\ResourceDeletedData) instead ofResponse::success(...). The enveloped body is byte-identical to before: thedatapayload keeps its ownaffected/success/messagekeys (public promoted properties serialized intodata), while the distinct outer envelope message ('Resource deleted successfully') is supplied viaHasResponseMessageand stored in a private property so it is not leaked intodata.ResourceController::index()was intentionally left unchanged — itsResponse::successWithMeta()body flattens the repository's pagination meta (current_page, per_page, total, last_page, has_more, from, to, execution_time_ms), a key set that differs fromPaginatedResponse/Response::paginated()(which emitstotal_pages, has_next_page, has_previous_pageinstead oflast_page, has_more, from, to, execution_time_ms); migrating would drop and add keys, breaking the response contract. Permission-denied / not-found paths still return a plainResponse. - Reference example:
UploadController'sinfo/signedUrl/deleteendpoints now return typed response DTOs (Glueful\Controllers\DTOs\BlobInfoData,SignedUrlData,BlobDeletedData) instead ofResponse::success(...). The enveloped bodies are byte-identical to before —BlobInfoDatauses the serializer'stoArray()escape hatch to pass the blob row through verbatim (preserving arbitrary column order and the omit-when-absentnative_urlkey),SignedUrlDatadeclaresnative_urlas an uninitialized typed property (skipped by the serializer when absent, emitted last when present), and each DTO supplies its envelopemessageviaHasResponseMessage.notFound/unauthorized/disabled paths still return a plainResponse. - Reference example:
HealthController::readiness()now returns a typed response DTO (Glueful\Controllers\DTOs\ReadinessData,ResponseData+HasResponseMessage) on its 200 success path — body byte-identical (the per-requesttimestamppasses through unchanged); the 503 "not ready" branch still returns a plainResponse::error(...).HealthController::database()was intentionally NOT migrated: it wraps its success inprivateCached()to attachCache-Controlheaders, which theResponseDatareturn path cannot carry — so endpoints that set response-level headers (caching, custom headers) stay manual. This is a documented convention boundary: a typedResponseDatareturn controls body + status + message, not headers. - OpenAPI:
DocGeneratorgains explicit-file-list merge methods. AddedDocGenerator::generateFromExtensionFiles(array $files)andgenerateFromRouteFiles(array $files), which merge a caller-supplied list of fragment files instead of globbing a directory. The existinggenerateFromExtensions(?string $path)/generateFromRoutes(?string $path)glob methods are unchanged and remain available for backward compatibility.
Removed
- OpenAPI: the legacy
comments(docblock-parsing) generator has been removed;reflectis now the only generator. TheCommentsDocGenerator(PHPDoc/@route/@responsedocblock and JSON-fragment route parsing) is deleted, along with thedocumentation.generatorconfig switch and theAPI_DOCS_GENERATORenv var — the code-firstreflectgenerator (introduced earlier in this same release) is now unconditional — its prior opt-in default of'comments'no longer applies.OpenApiGeneratorderives the spec'spathssolely from the live route table; its constructor no longer accepts aCommentsDocGeneratorargument (signature is now(ApplicationContext $context, ?DocGenerator $docGenerator = null, ?FileFinder $fileFinder = null, bool $runFromConsole = false, ?ResourceRouteExpander $resourceExpander = null)), and thegetCommentsGenerator()accessor andprocessExtensionAndRouteDocs()orchestration are gone. Preserved: resource-route expansion (include_resource_routes); route-origin filtering (documentation.sources.include_framework_routes/documentation.options.include_extensions, now read by the reflect generator); and merging of hand-authored custom OpenAPI JSON fragments placed at the TOP LEVEL ofdocs/json-definitions/(viaDocGenerator::generateFromDocJson, still run). Thejson-definitions/{routes,extensions}/subdirectories — which held the comment generator's per-file output — are intentionally NOT merged (re-merging that stale machine output would resurrect routes the reflect generator now derives live); the list-merge helpersDocGenerator::generateFromExtensionFiles/generateFromRouteFilesremain available but are no longer invoked by the pipeline. The@*route docblocks inroutes/*.phpare now inert (no longer parsed) but harmless. Operators who setAPI_DOCS_GENERATORshould remove it; the value is ignored.
Upgrade Notes
- The comment-based OpenAPI generator has been removed.
reflectis now the only OpenAPI generator.documentation.generatorandAPI_DOCS_GENERATORare no longer supported.- Route
@route,@summary,@requestBody,@response, and related docblock annotations are no longer read. - Use typed DTOs,
#[ApiOperation],#[QueryParam],#[ApiRequestBody], and#[ApiResponse]for OpenAPI documentation.