|
| 1 | +--- |
| 2 | +title: Components |
| 3 | +description: 'Centralize reusable definitions and reference them with $ref' |
| 4 | +icon: 'box' |
| 5 | +--- |
| 6 | + |
| 7 | +## Why Components? |
| 8 | + |
| 9 | +The `components` section is a central registry for anything you want to reuse. Once registered, any object can be referenced anywhere in the document with a `$ref` pointer instead of being repeated inline. This means a schema change only needs to happen in one place, and documentation tools render the component name as a clickable type link. |
| 10 | + |
| 11 | +```php |
| 12 | +use Cortex\OpenApi\Objects\Components; |
| 13 | + |
| 14 | +$components = Components::create() |
| 15 | + ->schema('User', $userSchema) |
| 16 | + ->response('NotFound', Response::notFound()->content(MediaType::json($errorSchema))) |
| 17 | + ->parameter('PageSize', Parameter::query('pageSize', Schema::integer()->minimum(1)->maximum(100))) |
| 18 | + ->securityScheme('BearerAuth', SecurityScheme::http('bearer')->bearerFormat('JWT')); |
| 19 | +``` |
| 20 | + |
| 21 | +## What Can Be Registered |
| 22 | + |
| 23 | +Components supports ten registries, each addressable by its own `$ref` path: |
| 24 | + |
| 25 | +| Registry | Method | $ref prefix | |
| 26 | +|----------|--------|-------------| |
| 27 | +| Schemas | `->schema(name, schema)` | `#/components/schemas/` | |
| 28 | +| Responses | `->response(name, response)` | `#/components/responses/` | |
| 29 | +| Parameters | `->parameter(name, parameter)` | `#/components/parameters/` | |
| 30 | +| Examples | `->example(name, example)` | `#/components/examples/` | |
| 31 | +| Request Bodies | `->requestBody(name, body)` | `#/components/requestBodies/` | |
| 32 | +| Headers | `->header(name, header)` | `#/components/headers/` | |
| 33 | +| Security Schemes | `->securityScheme(name, scheme)` | `#/components/securitySchemes/` | |
| 34 | +| Links | `->link(name, link)` | `#/components/links/` | |
| 35 | +| Callbacks | `->callback(name, callback)` | `#/components/callbacks/` | |
| 36 | +| Path Items | `->pathItem(name, pathItem)` | `#/components/pathItems/` | |
| 37 | + |
| 38 | +## Schemas |
| 39 | + |
| 40 | +Register any `cortexphp/json-schema` schema or raw array by name: |
| 41 | + |
| 42 | +```php |
| 43 | +use Cortex\JsonSchema\Schema; |
| 44 | + |
| 45 | +$components = Components::create() |
| 46 | + ->schema('User', Schema::object()->properties( |
| 47 | + Schema::integer('id')->format('int64')->required(), |
| 48 | + Schema::string('name')->required(), |
| 49 | + Schema::string('email')->format('email')->required(), |
| 50 | + Schema::string('role')->enum(['admin', 'editor', 'viewer'])->default('viewer'), |
| 51 | + )) |
| 52 | + ->schema('Error', Schema::object()->properties( |
| 53 | + Schema::integer('code')->format('int32')->required(), |
| 54 | + Schema::string('message')->required(), |
| 55 | + )) |
| 56 | + ->schema('Pagination', Schema::object()->properties( |
| 57 | + Schema::integer('total')->required(), |
| 58 | + Schema::integer('page')->required(), |
| 59 | + Schema::integer('perPage')->required(), |
| 60 | + Schema::boolean('hasMore')->required(), |
| 61 | + )); |
| 62 | +``` |
| 63 | + |
| 64 | +Reference schemas anywhere with `Reference::to()`: |
| 65 | + |
| 66 | +```php |
| 67 | +use Cortex\OpenApi\Objects\Reference; |
| 68 | + |
| 69 | +// In a MediaType |
| 70 | +MediaType::json(Reference::to('#/components/schemas/User')) |
| 71 | + |
| 72 | +// In a Parameter schema slot |
| 73 | +Parameter::query('filter', Reference::to('#/components/schemas/Filter')) |
| 74 | + |
| 75 | +// In a composed schema (allOf/oneOf/anyOf) |
| 76 | +Schema::object()->allOf( |
| 77 | + Reference::to('#/components/schemas/BaseEntity'), |
| 78 | + Schema::object()->properties(Schema::string('title')), |
| 79 | +) |
| 80 | +``` |
| 81 | + |
| 82 | +## Parameters |
| 83 | + |
| 84 | +Shared parameters — particularly path parameters that appear on multiple paths — are ideal candidates for components: |
| 85 | + |
| 86 | +```php |
| 87 | +use Cortex\OpenApi\Objects\Parameter; |
| 88 | + |
| 89 | +$components = Components::create() |
| 90 | + ->parameter('UserId', |
| 91 | + Parameter::path('userId', Schema::integer()->format('int64')) |
| 92 | + ->description('The unique user identifier'), |
| 93 | + ) |
| 94 | + ->parameter('ArticleSlug', |
| 95 | + Parameter::path('slug', Schema::string()->pattern('^[a-z0-9-]+$')) |
| 96 | + ->description('The article slug'), |
| 97 | + ) |
| 98 | + ->parameter('PageSize', |
| 99 | + Parameter::query('pageSize', Schema::integer()->minimum(1)->maximum(100)->default(20)) |
| 100 | + ->description('Number of results per page'), |
| 101 | + ) |
| 102 | + ->parameter('PageNumber', |
| 103 | + Parameter::query('page', Schema::integer()->minimum(1)->default(1)) |
| 104 | + ->description('Page number'), |
| 105 | + ); |
| 106 | +``` |
| 107 | + |
| 108 | +Reference parameters from path items: |
| 109 | + |
| 110 | +```php |
| 111 | +use Cortex\OpenApi\Objects\PathItem; |
| 112 | + |
| 113 | +PathItem::create('/users/{userId}/articles/{slug}') |
| 114 | + ->parameters( |
| 115 | + Reference::to('#/components/parameters/UserId'), |
| 116 | + Reference::to('#/components/parameters/ArticleSlug'), |
| 117 | + ) |
| 118 | + ->operations( |
| 119 | + Operation::get() |
| 120 | + ->operationId('users.articles.show') |
| 121 | + ->parameters( |
| 122 | + Reference::to('#/components/parameters/PageSize'), |
| 123 | + Reference::to('#/components/parameters/PageNumber'), |
| 124 | + ), |
| 125 | + ); |
| 126 | +``` |
| 127 | + |
| 128 | +## Responses |
| 129 | + |
| 130 | +Centralize common error responses to avoid repeating them on every operation: |
| 131 | + |
| 132 | +```php |
| 133 | +use Cortex\OpenApi\Objects\Response; |
| 134 | +use Cortex\OpenApi\Objects\MediaType; |
| 135 | + |
| 136 | +$errorSchema = Reference::to('#/components/schemas/Error'); |
| 137 | + |
| 138 | +$components = Components::create() |
| 139 | + ->response('BadRequest', |
| 140 | + Response::badRequest()->content(MediaType::json($errorSchema)), |
| 141 | + ) |
| 142 | + ->response('Unauthorized', |
| 143 | + Response::unauthorized()->content(MediaType::json($errorSchema)), |
| 144 | + ) |
| 145 | + ->response('Forbidden', |
| 146 | + Response::forbidden()->content(MediaType::json($errorSchema)), |
| 147 | + ) |
| 148 | + ->response('NotFound', |
| 149 | + Response::notFound()->content(MediaType::json($errorSchema)), |
| 150 | + ) |
| 151 | + ->response('UnprocessableEntity', |
| 152 | + Response::unprocessable()->content(MediaType::json($errorSchema)), |
| 153 | + ); |
| 154 | +``` |
| 155 | + |
| 156 | +Reference them from operations using `Response::ref()`: |
| 157 | + |
| 158 | +```php |
| 159 | +Operation::get() |
| 160 | + ->responses( |
| 161 | + Response::ok()->content(MediaType::json(Reference::to('#/components/schemas/User'))), |
| 162 | + Response::ref('#/components/responses/NotFound'), |
| 163 | + Response::ref('#/components/responses/Unauthorized'), |
| 164 | + ); |
| 165 | +``` |
| 166 | + |
| 167 | +## Request Bodies |
| 168 | + |
| 169 | +Shared request body definitions are useful when multiple operations accept the same payload shape: |
| 170 | + |
| 171 | +```php |
| 172 | +use Cortex\OpenApi\Objects\RequestBody; |
| 173 | + |
| 174 | +$components = Components::create() |
| 175 | + ->requestBody('CreateUser', |
| 176 | + RequestBody::create() |
| 177 | + ->required(true) |
| 178 | + ->content(MediaType::json(Reference::to('#/components/schemas/UserInput'))), |
| 179 | + ) |
| 180 | + ->requestBody('UpdateUser', |
| 181 | + RequestBody::create() |
| 182 | + ->required(true) |
| 183 | + ->content(MediaType::json(Reference::to('#/components/schemas/UserPatch'))), |
| 184 | + ); |
| 185 | + |
| 186 | +// Reference |
| 187 | +Operation::post()->requestBody(RequestBody::ref('#/components/requestBodies/CreateUser')); |
| 188 | +Operation::patch()->requestBody(RequestBody::ref('#/components/requestBodies/UpdateUser')); |
| 189 | +``` |
| 190 | + |
| 191 | +## Security Schemes |
| 192 | + |
| 193 | +Security schemes are almost always defined in components and referenced from operations or the document root: |
| 194 | + |
| 195 | +```php |
| 196 | +use Cortex\OpenApi\Objects\SecurityScheme; |
| 197 | +use Cortex\OpenApi\Objects\OAuthFlows; |
| 198 | +use Cortex\OpenApi\Objects\OAuthFlow; |
| 199 | +use Cortex\OpenApi\Enums\In; |
| 200 | + |
| 201 | +$components = Components::create() |
| 202 | + ->securityScheme('BearerAuth', |
| 203 | + SecurityScheme::http('bearer') |
| 204 | + ->bearerFormat('JWT') |
| 205 | + ->description('JWT Bearer token from /auth/token'), |
| 206 | + ) |
| 207 | + ->securityScheme('ApiKey', |
| 208 | + SecurityScheme::apiKey('X-API-Key', In::Header) |
| 209 | + ->description('API key passed in the X-API-Key header'), |
| 210 | + ) |
| 211 | + ->securityScheme('OAuth2', |
| 212 | + SecurityScheme::oauth2( |
| 213 | + OAuthFlows::create()->authorizationCode( |
| 214 | + OAuthFlow::create() |
| 215 | + ->authorizationUrl('https://auth.example.com/oauth/authorize') |
| 216 | + ->tokenUrl('https://auth.example.com/oauth/token') |
| 217 | + ->scopes([ |
| 218 | + 'read:users' => 'Read user profiles', |
| 219 | + 'write:users' => 'Create and modify users', |
| 220 | + ]), |
| 221 | + ), |
| 222 | + ), |
| 223 | + ); |
| 224 | +``` |
| 225 | + |
| 226 | +See [Security](/openapi/security) for full coverage of authentication schemes. |
| 227 | + |
| 228 | +## Headers |
| 229 | + |
| 230 | +Shared response headers — such as rate-limit or tracing headers — can be registered once: |
| 231 | + |
| 232 | +```php |
| 233 | +use Cortex\OpenApi\Objects\Header; |
| 234 | + |
| 235 | +$components = Components::create() |
| 236 | + ->header('X-Request-Id', |
| 237 | + Header::create() |
| 238 | + ->schema(Schema::string()->format('uuid')) |
| 239 | + ->description('Echoed back for distributed tracing'), |
| 240 | + ) |
| 241 | + ->header('X-RateLimit-Remaining', |
| 242 | + Header::create() |
| 243 | + ->schema(Schema::integer()) |
| 244 | + ->description('Number of requests remaining in the current window'), |
| 245 | + ); |
| 246 | +``` |
| 247 | + |
| 248 | +Reference headers in a response's `->headers()` array: |
| 249 | + |
| 250 | +```php |
| 251 | +Response::ok()->headers([ |
| 252 | + 'X-Request-Id' => Reference::to('#/components/headers/X-Request-Id'), |
| 253 | + 'X-RateLimit-Remaining' => Reference::to('#/components/headers/X-RateLimit-Remaining'), |
| 254 | +]) |
| 255 | +``` |
| 256 | + |
| 257 | +## Examples |
| 258 | + |
| 259 | +Register named examples in components for reuse across multiple parameters, request bodies, and responses: |
| 260 | + |
| 261 | +```php |
| 262 | +use Cortex\OpenApi\Objects\Example; |
| 263 | + |
| 264 | +$components = Components::create() |
| 265 | + ->example('AdminUser', Example::create() |
| 266 | + ->summary('An admin user') |
| 267 | + ->value(['id' => 1, 'name' => 'Admin', 'role' => 'admin'])) |
| 268 | + ->example('EditorUser', Example::create() |
| 269 | + ->summary('An editor user') |
| 270 | + ->value(['id' => 2, 'name' => 'Editor', 'role' => 'editor'])); |
| 271 | +``` |
| 272 | + |
| 273 | +## Path Items |
| 274 | + |
| 275 | +`pathItems` in components is a newer addition (OpenAPI 3.1) that lets you define reusable path item templates — useful for webhooks: |
| 276 | + |
| 277 | +```php |
| 278 | +$components = Components::create() |
| 279 | + ->pathItem('UserEvent', |
| 280 | + PathItem::create('') |
| 281 | + ->operations( |
| 282 | + Operation::post() |
| 283 | + ->operationId('webhook.user.event') |
| 284 | + ->requestBody( |
| 285 | + RequestBody::create() |
| 286 | + ->content(MediaType::json(Reference::to('#/components/schemas/UserEvent'))), |
| 287 | + ) |
| 288 | + ->responses(Response::ok()), |
| 289 | + ), |
| 290 | + ); |
| 291 | +``` |
| 292 | + |
| 293 | +## Putting It Together |
| 294 | + |
| 295 | +A realistic components setup combining schemas, security, and shared responses: |
| 296 | + |
| 297 | +```php |
| 298 | +use Cortex\JsonSchema\Schema; |
| 299 | +use Cortex\OpenApi\Objects\Components; |
| 300 | +use Cortex\OpenApi\Objects\Parameter; |
| 301 | +use Cortex\OpenApi\Objects\Response; |
| 302 | +use Cortex\OpenApi\Objects\MediaType; |
| 303 | +use Cortex\OpenApi\Objects\SecurityScheme; |
| 304 | +use Cortex\OpenApi\Objects\OAuthFlows; |
| 305 | +use Cortex\OpenApi\Objects\OAuthFlow; |
| 306 | +use Cortex\OpenApi\Objects\Reference; |
| 307 | + |
| 308 | +$components = Components::create() |
| 309 | + // Schemas |
| 310 | + ->schema('User', Schema::object()->properties( |
| 311 | + Schema::integer('id')->required(), |
| 312 | + Schema::string('name')->required(), |
| 313 | + Schema::string('email')->format('email')->required(), |
| 314 | + )) |
| 315 | + ->schema('Error', Schema::object()->properties( |
| 316 | + Schema::string('message')->required(), |
| 317 | + Schema::array('errors')->items(Schema::string()), |
| 318 | + )) |
| 319 | + |
| 320 | + // Shared parameters |
| 321 | + ->parameter('PageSize', |
| 322 | + Parameter::query('pageSize', Schema::integer()->minimum(1)->maximum(100)->default(20)), |
| 323 | + ) |
| 324 | + ->parameter('PageNumber', |
| 325 | + Parameter::query('page', Schema::integer()->minimum(1)->default(1)), |
| 326 | + ) |
| 327 | + |
| 328 | + // Shared responses |
| 329 | + ->response('NotFound', |
| 330 | + Response::notFound()->content( |
| 331 | + MediaType::json(Reference::to('#/components/schemas/Error')), |
| 332 | + ), |
| 333 | + ) |
| 334 | + ->response('Unauthorized', |
| 335 | + Response::unauthorized()->content( |
| 336 | + MediaType::json(Reference::to('#/components/schemas/Error')), |
| 337 | + ), |
| 338 | + ) |
| 339 | + |
| 340 | + // Security |
| 341 | + ->securityScheme('BearerAuth', |
| 342 | + SecurityScheme::http('bearer')->bearerFormat('JWT'), |
| 343 | + ); |
| 344 | +``` |
0 commit comments