Generating documentation in Symfony

Dev Diary

Documentation is important in the field of software development. It is the primary source for figuring out how a certain library, framework or application (programming) interface works. At the same time, you need to keep it up to date. This can sometimes feel like redundant work. In a lot of cases, this redundancy can be avoided.

Author
Arjan Frans
Date
June 28, 2023
Reading time
3 Minutes

Source code is documentation

Your source code will always be the best documentation. At least for fellow programmers who understand the language. If your software is aimed to be used by other software developers anyway, why should you even bother with writing documentation? Sometimes it can feel like you're doing everything twice: you write your code and then you write your documentation which describes the same thing.

In the PHP/Symfony ecosystem, we developed a few tools that can help with generating automated documentation. Below, you will find a brief description of how it works. To skip right to the full example, check out this repository.

Documenting routes

A PHP implementation of the OpenAPI Standard along with the NelmioApiDocBundle makes it possible to easily generate documentation from your controller endpoints. Here is an example of how it can be used:

#[Route('/{id}', methods: 'POST')] #[OA\RequestBody( required: true, content: new Model(type: UpdateContactRequest::class))] #[OA\Response( response: 200, description: 'ContactResponse', content: new Model(type: ContactResponse::class), )]

As you can tell, there's quite a bit of code needed to fully document the controller action. It also seems redundant to use the same models in the actual code when they're already strictly typed.

Typed input and output

Incoming requests are converted into strictly typed objects by using our http-kernel-extensions library. Using the `#[FromRequest]` attribute we can indicate which argument is supposed to be the incoming request object.

public function exampleAction( #[FromRequest] UpdateContactRequest $request // Automatically mapped input model ): ContactResponse { // .. }

To be able to return an object directly from a controller we can utilize Symfony's kernel.view event. Inside an event subscriber we can then convert the object into an actual response.

final class ViewEventSubscriber implements EventSubscriberInterface { public function __construct(private readonly NormalizerInterface $normalizer) { } public function onKernelView(ViewEvent $event): void { $view = $event->getControllerResult(); if (null === $view) { $response = new JsonResponse('', Response::HTTP_NO_CONTENT); } else { $response = new JsonResponse($this->normalizer->normalize($view)); } $event->setResponse($response); } public static function getSubscribedEvents(): array { return [ KernelEvents::VIEW => ['onKernelView'], ]; } }

The "Documented" Route

To prevent redundant type definitions, we developed a custom route attribute in our api-documentation-bundle. It automatically reads the input and output types defined in the function signature or docblocks in case of arrays or generics. A controller action can now look like the following and will automatically be documented without any extra annotations.

#[DocumentedRoute('/{id}', methods: 'PATCH')] public function updateContactAction(#[FromRequest] UpdateContactRequest $request): ContactResponse { // ... }

If you now check out the generated documentation it will automatically show all the strictly typed objects for input and output.

screenshot_unmarked
The api-documentation-bundle does provide some more options than shown above. For example manually setting the types or the description.

Conclusion

By combining the two bundles we developed we have reduced the redundancy of having to define the input and output type multiple times. Input and output are strictly typed and most of the endpoints, which already have their obvious usage described in their name, now require less code. All the OpenApi attributes required for these use cases have been abstracted away. Flexibility is retained by allowing you to still use all the OpenApi attributes in combination with the DocumentedRoute attribute.

Check out the complete example.

More of that?

Contact form

*Required field
*Required field
*Required field
*Required field
We protect your privacy

We keep your personal data safe and do not share it with third parties. You can find out more about this in our privacy policy.