How To Generate a Swagger/OpenAPI Spec for your Laravel API
You’re investing in your API, and that means finally creating an OpenAPI spec that accurately describes your API. Of course you could write the document by hand, or use a GUI tool to make it easier. Or with just a bit of upfront work, you can generate a complete OpenAPI specification directly from your Laravel application.
Back in the time of Swagger documents, BeyondCode had a well known package for performing spec generation. However, with the advent of OpenAPI, a new package, Scribe (opens in a new tab), has become the go to for generating an OpenAPI Spec from a Laravel API.
An Overview of Scribe
Scribe is a robust documentation solution for PHP APIs. It helps you generate comprehensive human-readable documentation from your Laravel/Lumen/Dingo codebase. That includes HTML documentation, code samples, Postman Collections, and most importantly in our case, OpenAPI specifications.
So let’s start by installing the package using composer, and explore the options available.
composer require --dev knuckleswtf/scribe
Once installed, we want to publish the package configuration so that we can make any changes in how we want this to work.
php artisan vendor:publish --tag=scribe-config
Let’s take a quick look at the specific configuration options that will help us optimize this package to work with our Laravel API:
routes
- this option allows us to configure how we want to detect the API routes, and prefix we may use and any routes we want to exclude by default.type
- we can choose betweenstatic
(a static HTMI page) andlaravel
(a Blade view). We will get into more details on the differences later.openapi
****- ****this section allows you to toggle OpenAPI generation on or off. We’ll toggle it on.auth
- specify the API’s authentication mechanism. This will be used to describe thesecurity
section of the OpenAPI documentstrategies
- this is where we configure how Scribe will interact with our application to get the data needed to create the specification and documentation.
Scribe’s Default OpenAPI Output
As mentioned above, the type
config allows us to specify the type of output we get, and also where we want it to be outputted. The static
option will generate HTMI documentation pages within our public
directory. laravel
we will generate this within the storage
directory.
Note, anything in the storage
directory typically won’t be committed to version control - so you would need to update the .gitignore
file if you want to version this. For this article, we will keep the defaults as we want to focus on the OpenAPI Specification not the documentation.
Testing Generation
When we first install and set up this package, we should run a test to make sure that everything is configured correctly and we aren’t going to run into issues further on.
php artisan scribe:generate
You should see a console output similar to the following:
❯ php artisan scribe:generateⓘ Processing route: [GET] api/user✔ Processed route: [GET] api/userⓘ Extracting intro and auth Markdown files to: .scribe✔ Extracted intro and auth Markdown files to: .scribeⓘ Writing HTML docs...✔ Wrote HTML docs and assets to: public/docs/ⓘ Generating Postman collection✔ Wrote Postman collection to: public/docs/collection.jsonⓘ Generating OpenAPI specification✔ Wrote OpenAPI specification to: public/docs/openapi.yamlChecking for any pending upgrades to your config file...✔ Visit your docs at http://localhost/docs
So, we can confirm that our package is working correctly with our application, let’s take a look at the OpenAPI Specification that was generated and see what changes are required.
openapi: 3.0.3info: title: Laravel description: '' version: 1.0.0servers: - url: 'http://localhost'paths: /api/user: get: summary: '' operationId: getApiUser description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: []tags: - name: Endpoints description: ''
This is our default set up in Laravel - we are using a project I have yet to add an API to. Let’s add some endpoints so we can see something a little more fleshed out.
Our Example App: The Standup API
We’ll be working on an asynchronous stand-up application, it allows you to do your daily check-ins on one system, enables your manager to have a high level overview of team blockers and mood etc. You can follow along with the GitHub repository here (opens in a new tab).
Non-Optimized Example Output
Now we got that out of the way, let’s regenerate our OpenAPI Specification now that I have added BREAD (Browse, Read, Edit, Add, Delete) endpoints.
openapi: 3.0.3info: title: Laravel description: '' version: 1.0.0servers: - url: 'http://localhost'paths: /api/standups: get: summary: '' operationId: getApiStandups description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] post: summary: '' operationId: postApiStandups description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: mood: type: string description: '' example: excited enum: - happy - sad - excited - frustrated - tired - neutral - angry - anxious - optimistic - pensive - surprised - sick - confident - disappointed - amused - relieved - indifferent - grateful - inspired - confused tasks: type: string description: 'Must be at least 2 characters.' example: bhwfcupupgcgexmeiuzxvftnsxzwcvllulcenigndwkejgeqjalhsmrsseu blockers: type: string description: 'Must be at least 2 characters.' example: cwtdgfoqgixwkwhlrwzapudsxtrtoiuldf questions: type: string description: 'Must be at least 2 characters.' example: nqfytjwyyyxv comments: type: string description: 'Must be at least 2 characters.' example: ynztxjgszeqzhdqamrfvtnsajozigaivnxbjsrvdujrchjnq department: type: string description: '' example: quo required: - mood - tasks - department security: [] '/api/standups/{uuid}': get: summary: '' operationId: getApiStandupsUuid description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: uuid description: '' example: eb68e6e5-999a-3a67-a465-afa4b064af3d required: true schema: type: string '/api/standups/{standUp_id}': put: summary: '' operationId: putApiStandupsStandUp_id description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: mood: type: string description: '' example: pensive enum: - happy - sad - excited - frustrated - tired - neutral - angry - anxious - optimistic - pensive - surprised - sick - confident - disappointed - amused - relieved - indifferent - grateful - inspired - confused tasks: type: string description: 'Must be at least 2 characters.' example: zeovcuepgdsmjpzdjtycdvcbhkeoxvifmj blockers: type: string description: 'Must be at least 2 characters.' example: eqursmzxxjivpjqphrlxhritykekqhgsunqbtgvwvypyumuyekvxgzvcviyqa questions: type: string description: 'Must be at least 2 characters.' example: nwqwclebngkisgxklxnaqrncxpkpzicwplklzpstkrnltjiivjbgmvybbgctihycvwtveebvytrk comments: type: string description: 'Must be at least 2 characters.' example: vjkatsczlriwefgtiovegcovtzxcngsbiirsyegkfsegwjaandugmbx department: type: string description: '' example: optio required: - mood - tasks - department security: [] delete: summary: '' operationId: deleteApiStandupsStandUp_id description: '' parameters: [] responses: { } tags: - Endpoints security: [] parameters: - in: path name: standUp_id description: 'The ID of the standUp.' example: a required: true schema: type: stringtags: - name: Endpoints description: ''
That was a lot to look through, so let’s do a run through of each path - so we can understand what all this YAML actually means.
Documenting all Response Codes
Let’s focus on the browse endpoint, accessed through /api/standups
, which returns a collection of stand-ups that are part of the department you belong to.
/api/standups: get: summary: '' operationId: getApiStandups description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: []
You may have noticed that by default Scribe is only documenting the error responses of the API; it's missing how the API would respond successfully.
How Scribe Works
This is a good opportunity to explain about how Scribe works under the hood. Scribe scans your application routes to identify which endpoints should be documented based on your configuration. It then extracts metadata from your routes, such as route names, URI patterns, HTTP methods, and any specific annotations or comments in the controller that might be relevant for documentation.
Scribe then uses the extracted metadata to perform request simulation on your API. It captures the responses that come back, including: status codes, headers, and body content. All this then get packaged into an internal representation of your API, which is how the OpenAPI spec is created.
In the example above, only the 401
is being documented because Scribe hasn’t been configured with the proper authentication information, which makes it unable to access the proper response.
Getting to 200
Let’s modify our Laravel code to get some useful information about our 200
responses.
To achieve this we will use PHP 8.0 Attributes to add additional information to our controllers, this will use the built in Laravel ecosystem to make a request, inspect the information, and write the specification for you. Let’s have a look at our controller:
Adding tags
In OpenAPI, tags
are used to group related operations together. Typically, a good way to use tags is to have one tag per "resource" and then associate all the relevant operations that access and modify that resourc together. We'll add a group annotation to the top of the controller.
#[Group(name: 'Stand Ups', description: 'A series of endpoints that allow programatic access to managing stand-ups.', authenticated: true)]final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[Authenticated] #[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)] #[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
Authenicate Scribe
Let’s next focus on the invoke
method that is what will be used to generate the path information. We use #[Authenticated]
to let Scribe know that this endpoint needs to be authenticated
#[Group(name: 'Stand Ups', description: 'A series of endpoints that allow programatic access to managing stand-ups.', authenticated: true)]final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[Authenticated] #[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)] #[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
Add Descriptions
Use #[Endpoint]
to add additional information about this endpoint; describing what it’s function is.
#[Group(name: 'Stand Ups', description: 'A series of endpoints that allow programatic access to managing stand-ups.', authenticated: true)]final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[Authenticated] #[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)] #[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
Adding Responses
Finally, we want to add #[ResponseFromApiResource]
to let Scribe know how this API should respond, passing through the resource class and the model itself so Scribe can make a request in the background and inspect the types on the response payload. Also, we pass the boolean flag for whether or not this response should return a collection or not.
#[Group(name: 'Stand Ups', description: 'A series of endpoints that allow programatic access to managing stand-ups.', authenticated: true)]final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[Authenticated] #[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)] #[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
Adding tags
In OpenAPI, tags
are used to group related operations together. Typically, a good way to use tags is to have one tag per "resource" and then associate all the relevant operations that access and modify that resourc together. We'll add a group annotation to the top of the controller.
Authenicate Scribe
Let’s next focus on the invoke
method that is what will be used to generate the path information. We use #[Authenticated]
to let Scribe know that this endpoint needs to be authenticated
Add Descriptions
Use #[Endpoint]
to add additional information about this endpoint; describing what it’s function is.
Adding Responses
Finally, we want to add #[ResponseFromApiResource]
to let Scribe know how this API should respond, passing through the resource class and the model itself so Scribe can make a request in the background and inspect the types on the response payload. Also, we pass the boolean flag for whether or not this response should return a collection or not.
#[Group(name: 'Stand Ups', description: 'A series of endpoints that allow programatic access to managing stand-ups.', authenticated: true)]final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[Authenticated] #[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)] #[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
Now let’s see the OpenAPI spec:
/api/standups: get: summary: 'Browse Stand Ups' operationId: browseStandUps description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: '' type: standUps attributes: mood: angry tasks: "WOULD always get into that lovely garden. First, however, she went slowly after it: 'I never saw one, or heard of \"Uglification,\"' Alice ventured to ask. 'Suppose we change the subject. 'Go on with." blockers: "I wonder what was the matter on, What would become of you? I gave her answer. 'They're done with a little girl or a worm. The question is, Who in the pool as it was all ridges and furrows; the balls." questions: 'I to get dry again: they had to stop and untwist it. After a minute or two, they began moving about again, and put it into his cup of tea, and looked at it, and kept doubling itself up very sulkily.' comments: "Alice, in a large canvas bag, which tied up at this moment the door with his knuckles. It was as much as she ran. 'How surprised he'll be when he sneezes; For he can EVEN finish, if he doesn't." created: human: null timestamp: null string: null local: null - id: '' type: standUps attributes: mood: pensive tasks: "She was looking at the Lizard as she picked her way into a graceful zigzag, and was looking down at her for a good many little girls eat eggs quite as much as she couldn't answer either question, it." blockers: "Alice, 'it's very rude.' The Hatter opened his eyes were nearly out of sight, they were all ornamented with hearts. Next came the guests, mostly Kings and Queens, and among them Alice recognised the." questions: "After a while she was near enough to look over their slates; 'but it doesn't matter much,' thought Alice, 'shall I NEVER get any older than I am in the last few minutes that she had felt quite." comments: "An obstacle that came between Him, and ourselves, and it. Don't let him know she liked them best, For this must ever be A secret, kept from all the other was sitting on a little hot tea upon its." created: human: null timestamp: null string: null local: null properties: data: type: array example: - id: '' type: standUps attributes: mood: angry tasks: "WOULD always get into that lovely garden. First, however, she went slowly after it: 'I never saw one, or heard of \"Uglification,\"' Alice ventured to ask. 'Suppose we change the subject. 'Go on with." blockers: "I wonder what was the matter on, What would become of you? I gave her answer. 'They're done with a little girl or a worm. The question is, Who in the pool as it was all ridges and furrows; the balls." questions: 'I to get dry again: they had to stop and untwist it. After a minute or two, they began moving about again, and put it into his cup of tea, and looked at it, and kept doubling itself up very sulkily.' comments: "Alice, in a large canvas bag, which tied up at this moment the door with his knuckles. It was as much as she ran. 'How surprised he'll be when he sneezes; For he can EVEN finish, if he doesn't." created: human: null timestamp: null string: null local: null - id: '' type: standUps attributes: mood: pensive tasks: "She was looking at the Lizard as she picked her way into a graceful zigzag, and was looking down at her for a good many little girls eat eggs quite as much as she couldn't answer either question, it." blockers: "Alice, 'it's very rude.' The Hatter opened his eyes were nearly out of sight, they were all ornamented with hearts. Next came the guests, mostly Kings and Queens, and among them Alice recognised the." questions: "After a while she was near enough to look over their slates; 'but it doesn't matter much,' thought Alice, 'shall I NEVER get any older than I am in the last few minutes that she had felt quite." comments: "An obstacle that came between Him, and ourselves, and it. Don't let him know she liked them best, For this must ever be A secret, kept from all the other was sitting on a little hot tea upon its." created: human: null timestamp: null string: null local: null items: type: object properties: id: type: string example: '' type: type: string example: standUps attributes: type: object properties: mood: type: string example: angry tasks: type: string example: "WOULD always get into that lovely garden. First, however, she went slowly after it: 'I never saw one, or heard of \"Uglification,\"' Alice ventured to ask. 'Suppose we change the subject. 'Go on with." blockers: type: string example: "I wonder what was the matter on, What would become of you? I gave her answer. 'They're done with a little girl or a worm. The question is, Who in the pool as it was all ridges and furrows; the balls." questions: type: string example: 'I to get dry again: they had to stop and untwist it. After a minute or two, they began moving about again, and put it into his cup of tea, and looked at it, and kept doubling itself up very sulkily.' comments: type: string example: "Alice, in a large canvas bag, which tied up at this moment the door with his knuckles. It was as much as she ran. 'How surprised he'll be when he sneezes; For he can EVEN finish, if he doesn't." created: type: object properties: human: type: string example: null timestamp: type: string example: null string: type: string example: null local: type: string example: null tags: - 'Stand Ups'
Documenting Parameters
So far so good! However, this API example is limited. What if we add query parameters like filtering and sorting which we would likely want on a real API.
In terms of Laravel implementation, we recommend use the spatie/laravel-query-builder
package to enable JSON:API style filtering on my API, as it ties directly into Eloquent ORM from the request parameters. Let’s start adding some filters.
Our controller code used our StandUpRepository
which just leverages Eloquent to query our database through a shared abstraction. However, we want to lean on the package by Spatie, which has a slightly different approach. Let’s rewrite this code to make it more flexible.
#[Authenticated]#[Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.')]#[ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true)]public function __invoke(Request $request): CollectionResponse{ $standups = QueryBuilder::for( subject: $this->repository->forTeam( team: $this->auth->user()->current_team_id, ), )->allowedFilters( filters: $this->repository->filters(), )->allowedIncludes( includes: $this->repository->includes(), )->allowedSorts( sorts: $this->repository->sort(), )->getEloquentBuilder(); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), );}
We use the QueryBuilder
class from the package, to pass in the result of our repository call. The repository is just passing a pre-built query back, which we can use to paginate or extend as required. I prefer this approach as the sometimes you want to tie multiple methods together. You will see that I have 4 new methods that weren’t there before:
allowedFilters
allowedIncludes
allowedSorts
getEloquentBuilder
The first three allow you to programmatically control what parts of the query parameters to use and which to ignore. The final one is to get back the eloquent query builder, that we want to use as we know the API for it. The package returns a custom query builder, which does not have all of the methods we may want. Let’s flesh out the filter, include, and sort method calls next.
Going back we add attributes that will be parsed - so that our OpenAPI spec is generated with all available options:
final readonly class IndexController{ public function __construct( private AuthManager $auth, private StandUpRepository $repository, ) { } #[ Authenticated, QueryParam(name: 'filter[mood]', type: 'string', description: 'Filter the results by mood', required: false, example: 'filter[mood]=neutral', enum: Mood::class), QueryParam(name: 'filter[name]', type: 'string', description: 'Filter the results by the users name', required: false, example: 'filter[mood]=Rumpelstiltskin'), QueryParam(name: 'filter[department]', type: 'string', description: 'Filter the results by the department name', required: false, example: 'Engineering'), QueryParam(name: 'include', type: 'string', description: 'A comma separated list of relationships to side-load', required: false, example: 'include=user,department.team'), QueryParam(name: 'sort', type: 'string', description: 'Sort the results based on either the mood, or the created_at', required: false, example: 'sort=-mood'), ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: true), Endpoint(title: 'Browse Stand Ups', description: 'Browse through the stand-ups that belong to your team, no matter what department you are in.') ] public function __invoke(Request $request): CollectionResponse { $standups = $this->repository->forTeam( team: $this->auth->user()->current_team_id, ); return new CollectionResponse( data: StandUpResource::collection( resource: $standups->paginate(), ), ); }}
NOTE
You may have noticed that the syntax has collapsed all the metadata into one attribute. It’s just a code style choice, there’s no change in functionality.
The result of the above will be the following inside your OpenAPI specification:
parameters: - in: query name: 'filter[mood]' description: 'Filter the results by mood' example: 'filter[mood]=neutral' required: false schema: type: string description: 'Filter the results by mood' example: 'filter[mood]=neutral' enum: - happy - sad - excited - frustrated - tired - neutral - angry - anxious - optimistic - pensive - surprised - sick - confident - disappointed - amused - relieved - indifferent - grateful - inspired - confused - in: query name: 'filter[name]' description: 'Filter the results by the users name' example: 'filter[mood]=Rumpelstiltskin' required: false schema: type: string description: 'Filter the results by the users name' example: 'filter[mood]=Rumpelstiltskin' - in: query name: 'filter[department]' description: 'Filter the results by the department name' example: Engineering required: false schema: type: string description: 'Filter the results by the department name' example: Engineering - in: query name: include description: 'A comma separated list of relationships to side-load' example: 'include=user,department.team' required: false schema: type: string description: 'A comma separated list of relationships to side-load' example: 'include=user,department.team' - in: query name: sort description: 'Sort the results based on either the mood, or the created_at' example: sort=-mood required: false schema: type: string description: 'Sort the results based on either the mood, or the created_at' example: sort=-mood
Quite convenient I am sure you can agree!
A More Complex Endpoint
Let’s now move onto documenting our store
endpoint which is what is used to create a new stand-up.
/api/standups: post: summary: '' operationId: postApiStandups description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: mood: type: string description: '' example: excited enum: - happy - sad - excited - frustrated - tired - neutral - angry - anxious - optimistic - pensive - surprised - sick - confident - disappointed - amused - relieved - indifferent - grateful - inspired - confused tasks: type: string description: 'Must be at least 2 characters.' example: fsukiymcjmglqdyuuecbuhdlplot blockers: type: string description: 'Must be at least 2 characters.' example: xxqzeornblypfisimgvgucodtqracytnncacoqxqaeuzytrvmezydvztnqtmrmbgdebrfdmgkmjczytt questions: type: string description: 'Must be at least 2 characters.' example: ckmhwsbrdoryyfdxhidyrbugkaftcyiozxzsdtahbnsdivqferixcflplmadjarlyosbn comments: type: string description: 'Must be at least 2 characters.' example: kbczrybawedlzxhpzyhcorgzjmsgcdvdbgryjaqhwsbccxwyfkprfhnpogyqjuyyramuqrkzzsypaajoegiu department: type: string description: '' example: pariatur required: - mood - tasks - department security: []
For the most part, this has been documented quite well by leaning on the Laravel framework and understanding what the validation rules on the request means. Let’s enhance this by adding some information.
#[ Group(name: 'Stand Ups', description: 'A series of endpoints that allow programmatic access to managing stand-ups.', authenticated: true), Authenticated, Endpoint(title: 'Create a new Stand Up', description: 'Create a new Stand Up for a specified department, will be assigned to whichever user is authenticated at the time.'),]
This is similar to what we did on the IndexController
but this time we are jumping straight into grouping the attributes all together at the top of the class. We do not need to add these above the invoke
method, as this class only performs the one action anyway. I would consider moving these if I were to leverage additional Attributes for different purposes on the method, however for now I am not. Let’s now regenerate the OpenAPI Specification to see what the difference is, but this time I am going to omit the request validation information.
post: summary: 'Create a new Stand Up' operationId: createANewStandUp description: 'Create a new Stand Up for a specified department, will be assigned to whichever user is authenticated at the time.' parameters: [] responses: { } tags: - 'Stand Ups' requestBody: required: true content:
As you can see, the information is starting to build up based on the information we pass through to the PHP Attributes. Let’s start expanding on the request body and response information and build a better specification.
#[ Authenticated, Group(name: 'Stand Ups', description: 'A series of endpoints that allow programmatic access to managing stand-ups.', authenticated: true), Endpoint(title: 'Create a new Stand Up', description: 'Create a new Stand Up for a specified department, will be assigned to whichever user is authenticated at the time.'), BodyParam(name: 'mood', type: 'string', description: 'The mood of the user to be submitted to the stand-up.', required: true, example: 'neutral', enum: Mood::class), BodyParam(name: 'tasks', type: 'string', description: 'The list of tasks the user is planning on working on today. Markdown is supported.', required: false, example: 'Today I will be working on the OpenAPI Specification.'), BodyParam(name: 'blockers', type: 'string', description: 'A list of things that are blocking the user from progressing. Markdown is supported.', required: false, example: 'I am currently being blocked by front-end playing with crayons.'), BodyParam(name: 'questions', type: 'string', description: 'A list of questions that the user wants information on, these could be anything. Markdown is supported.', required: false, example: 'How much wood, could a woodchuck chuck, if a woodchuck, could chuck wood.'), BodyParam(name: 'comments', type: 'string', description: 'Any comments that the user wants to add to their stand-up that may be useful.', required: false, example: 'Going to the Dentist at 2pm, will make up hours later.'), BodyParam(name: 'department', type: 'string', description: 'The Unique Identifier for the department that the user is adding their stand up to.', required: true, example: '1234-1234-1234-1234'), ResponseFromApiResource(StandUpResource::class, StandUp::class, collection: false)]
Now we have the body parameters for this request, as well as how the API is expected to respond. We are currently only documenting the happy path - as we have yet to decide how we want to handle errors. This will create the following in your OpenAPI Specification:
post: summary: 'Create a new Stand Up' operationId: createANewStandUp description: 'Create a new Stand Up for a specified department, will be assigned to whichever user is authenticated at the time.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 9bce14db-cdd1-4a8a-86cb-e05f9f918d20 type: standUps attributes: mood: sick tasks: "Tortoise, if he were trying which word sounded best. Some of the deepest contempt. 'I've seen hatters before,' she said to Alice; and Alice looked all round the court and got behind him, and said." blockers: "Hatter with a soldier on each side, and opened their eyes and mouths so VERY remarkable in that; nor did Alice think it so quickly that the cause of this elegant thimble'; and, when it had made. 'He." questions: "Alice. 'Come on, then!' roared the Queen, 'and he shall tell you my adventures--beginning from this morning,' said Alice desperately: 'he's perfectly idiotic!' And she began again: 'Ou est ma." comments: 'Alice felt a little ledge of rock, and, as there was nothing else to say "HOW DOTH THE LITTLE BUSY BEE," but it was written to nobody, which isn''t usual, you know.'' Alice had never been so much.' created: human: '0 seconds ago' timestamp: 1713094155 string: '2024-04-14 11:29:15' local: '2024-04-14T11:29:15' properties: data: type: object properties: id: type: string example: 9bce14db-cdd1-4a8a-86cb-e05f9f918d20 type: type: string example: standUps attributes: type: object properties: mood: type: string example: sick tasks: type: string example: "Tortoise, if he were trying which word sounded best. Some of the deepest contempt. 'I've seen hatters before,' she said to Alice; and Alice looked all round the court and got behind him, and said." blockers: type: string example: "Hatter with a soldier on each side, and opened their eyes and mouths so VERY remarkable in that; nor did Alice think it so quickly that the cause of this elegant thimble'; and, when it had made. 'He." questions: type: string example: "Alice. 'Come on, then!' roared the Queen, 'and he shall tell you my adventures--beginning from this morning,' said Alice desperately: 'he's perfectly idiotic!' And she began again: 'Ou est ma." comments: type: string example: 'Alice felt a little ledge of rock, and, as there was nothing else to say "HOW DOTH THE LITTLE BUSY BEE," but it was written to nobody, which isn''t usual, you know.'' Alice had never been so much.' created: type: object properties: human: type: string example: '0 seconds ago' timestamp: type: integer example: 1713094155 string: type: string example: '2024-04-14 11:29:15' local: type: string example: '2024-04-14T11:29:15' tags: - 'Stand Ups' requestBody: required: true content: application/json: schema: type: object properties: mood: type: string description: 'The mood of the user to be submitted to the stand-up.' example: neutral enum: - happy - sad - excited - frustrated - tired - neutral - angry - anxious - optimistic - pensive - surprised - sick - confident - disappointed - amused - relieved - indifferent - grateful - inspired - confused tasks: type: string description: 'The list of tasks the user is planning on working on today. Markdown is supported.' example: 'Today I will be working on the OpenAPI Specification.' blockers: type: string description: 'A list of things that are blocking the user from progressing. Markdown is supported.' example: 'I am currently being blocked by front-end playing with crayons.' questions: type: string description: 'A list of questions that the user wants information on, these could be anything. Markdown is supported.' example: 'How much wood, could a woodchuck chuck, if a woodchuck, could chuck wood.' comments: type: string description: 'Any comments that the user wants to add to their stand-up that may be useful.' example: 'Going to the Dentist at 2pm, will make up hours later.' department: type: string description: 'The Unique Identifier for the department that the user is adding their stand up to.' example: 1234-1234-1234-1234 required: - mood - department
As you can see, a lot more information is provided which will help anyone who wants to interact with this API.
Summary
If we follow this approach throughout our API, we can generate a well documented OpenAPI Specification for our Laravel based API - utilizing modern PHP to add information to our code base. This not only aids in the OpenAPI generation, but it also adds a level of in-code documentation that will help onboard any new developer who needs to know what the purpose of an endpoint may be.