Customizing API Documentation
The API documentation feature is provided via the zf-apigility-documentation module. This module provides an object model of all captured documentation information, including:
- All APIs available.
- All Services available in each API.
- All Operations available in each API.
- All required/expected
Accept
andContent-Type
request headers and expectedContent-Type
response headers for each available API Service Operation. - All configured fields for each service.
Moreover, it provides a configurable MVC endpoint for returning documentation:
- Documentation is delivered in a serialized JSON structure when the
Accept
header specifies JSON. - Documentation is delivered in HTML when the
Accept
header specifies HTML. - End-users may configure alternate/additional formats via content-negotiation.
There are two ways to provide custom formats for your documentation:
- For HTML, XML, or other "markup" styles of documentaton, you can provide alternate view scripts
for the
ZF\Apigility\Documentation\Controller
. - For other formats, you can use content negotiation, providing an alternate view model and view renderer.
HTML Customization
If you want to customize the markup of your API documentation, you can have a look at the source code of the zf-apigility-documentation module. You will need to perform the following steps:
- Create a view script.
- Configure the view to use your view script.
Creating a view script
You need at least one view script to act as an entry point; this will be the view script selected by
the zf-apigility-documentation
controller and rendered by the application. That view script will
receive a type
variable which will indicate what documentation is being requested, and then
additional variables based on that type.
We recommend that your view script use the type to select additional view scripts to render; this
will allow you to reduce complexity in your view script. To see an example, look at the
Bootstrap-style view scripts
in the zf-apigility-documentation repository; the show.phtml
view script is the entry point for
all HTML views we provide. We highly recommend copying these view scripts and customizing them for
your markup, instead of starting from scratch.
If you decide to create your own view scripts, the following sections detail the variables present based on the type, and what operations are available.
apiList
The apiList
type will also have an apis
variable passed to the view. The apis
variable
contains a nested array structure:
[
[
'name' => 'API_Name',
'versions' => [
1,
2, // etc.
],
],
]
In other words, each item in the array is an associative array with two keys, name
and versions
.
api
The api
type will also have a documentation
variable passed to the view, which will be an
instance of
ZF\Apigility\Documentation\Api.
The following methods are exposed by that instance:
-
getName()
, which provides the API name -
getVersion()
, which provides the currently selected version -
getServices()
, which provides an iterable set of ZF\Apigility\Documentation\Service instances. See the next section for methods exposed by that object.
service
The service
type will have a documentation
variable passed to the view, which will be an
instance of ZF\Apigility\Documentation\Service.
The following methods are exposed by that instance:
-
getName()
, which returns the service name -
getDescription()
, which returns the service description -
getRoute()
returns the route match string -
getRouteIdentifierName()
returns the route segment name indicating the identifier -
getFields()
, which returns an iterable set of ZF\Apigility\Documentation\Field instances. -
getOperations()
, which returns an iterable set of ZF\Apigility\Documentation\Operation instances. For REST services, these will be the operations for collections. -
getEntityOperations()
, which, in the case of REST services, returns an iterable set ofOperation
instances for entities. -
getRequestAcceptTypes()
returns an array of allowedAccept
request header media types -
getRequestContentTypes()
returns an array of allowedContent-Type
request header media types -
getResponseStatusCodes()
returns an array of expected response status codes and reason phrases (an associative array with the memberscode
andmessage
)
Operation
instances expose the following methods:
-
getHttpMethod()
returns the HTTP method for the operation -
getDescription()
returns the operation description -
getRequestDescription()
returns the description for the operation request -
getResponseDescription()
returns the description for the operation response
Field
instances expose the following methods:
-
getName()
returns the field name -
getDescription()
returns the field description -
isRequired()
indicates whether or not the field is required
Available view helpers
zf-apigility-documentation
provides several view helpers related to documentation tasks. These
include:
-
agAcceptHeaders(ZF\Apigility\Documentation\Service $service)
: given a service, creates Bootstraplist-group-item
's of the allowedAccept
media types, properly escaped for HTML. -
agContentTypeHeaders(ZF\Apigility\Documentation\Service $service)
: just like the previous, but for allowedContent-Type
media types. -
agServicePath(ZF\Apigility\Documentation\Service $service, ZF\Apigility\Documentation\Operation $operation)
: given a service and operation, returns the URI for a given operation. -
agStatusCodes(ZF\Apigility\Documentation\Operation $operation)
: given an operation, returns a Bootstraplist-group
with the expected resposne status codes and reason phrases, properly escaped for HTML.
Configuring the view script
In order to expose your custom documentation markup, you will need to tell the Zend Framework 2 View Manager to select your own view script. This can be done with the following configuration:
[
'view_manager' => [
'template_map' => [
'zf-apigility-documentation/show' => 'path/to/your/custom/view_script.phtml',
],
],
]
This can be placed in a ZF2 module's configuration, or in your global application configuration
(e.g., config/autoload/global.php
).
Custom documentation formats via content negotiation
If you are providing a serialization form of documentation -- for example, a custom JSON representation, a YAML representation, etc. -- you may want to instead consider content negotiation.
Per the API primer, content negotiation is the act of matching
the Accept
request header to a response representation. In terms of Apigility, you will need to do
the following:
- Create a custom view model.
- Optionally, create a custom view renderer.
- Optionally, create a view strategy.
- Create content negotiation configuration mapping your view model to one or more
Accept
media types
Creating a view model
Creating a custom view model can be done by extending Zend\View\Model\ViewModel
, or any of the
other view model types present in Zend Framework 2. If you are providing a custom
JSON representation, we recommend extending ZF\ContentNegotiation\JsonModel
from the
zf-content-negotiation
model, as it provides features not present in the Zend Framework 2 variant
(including serialization of JsonSerializable
objects, detection of zf-hal
entity and collection
objects, and error handling).
Creating a view renderer
Creating a view renderer is only necessary if you have specialized serialization needs. As an
example, zf-hal
provides a specialized renderer for application/hal+json
, as it performs logic
for extracting objects to arrays, injecting relational links, and embedding resources. The
zf-apigility-documentation-swagger
module does not need to provide a renderer, as the standard
Zend\View\Renderer\JsonRenderer
is sufficient; it is able to customize the payload structure
easily in its custom ViewModel
and allow the JsonRenderer
to take care of the rest.
Creating a custom renderer means implementing Zend\View\Renderer\RendererInterface; and Apigility leaves such implementation as an exercise to the reader.
Creating a view strategy
Creating a view strategy means writing an event listener that will introspect the view model, determine if it matches, and then select and return a view renderer. The strategy optionally also provides an event listener that determines if it was responsible for selecting the view renderer, and, if so, injects the response object with appropriate headers.
The following is the SwaggerViewStrategy
from the zf-apigility-documentation-swagger
module; it
provides a typical example of a view strategy as used with Apigility:
namespace ZF\Apigility\Documentation\Swagger;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\View\Renderer\JsonRenderer;
use Zend\View\ViewEvent;
class SwaggerViewStrategy extends AbstractListenerAggregate
{
/**
* @var ViewModel
*/
protected $model;
/**
* @var JsonRenderer
*/
protected $renderer;
/**
* @param JsonRenderer $renderer
*/
public function __construct(JsonRenderer $renderer)
{
$this->renderer = $renderer;
}
/**
* @param EventManagerInterface $events
* @param int $priority
*/
public function attach(EventManagerInterface $events, $priority = 200)
{
$this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, [$this, 'selectRenderer'], $priority);
$this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, [$this, 'injectResponse'], $priority);
}
/**
* @param ViewEvent $e
* @return null|JsonRenderer
*/
public function selectRenderer(ViewEvent $e)
{
$model = $e->getModel();
if (! $model instanceof ViewModel) {
return;
}
$this->model = $model;
return $this->renderer;
}
/**
* @param ViewEvent $e
*/
public function injectResponse(ViewEvent $e)
{
if (! $this->model instanceof ViewModel) {
return;
}
$response = $e->getResponse();
if (! method_exists($response, 'getHeaders')) {
return;
}
$headers = $response->getHeaders();
$headers->addHeaderLine('Content-Type', 'application/vnd.swagger+json');
}
}
The above strategy selects the standard JsonRenderer
if the
ZF\Apigility\Documentation\Swagger\ViewModel
is detected for a view model. When the "response"
event is triggered, it checks to see if the selected view model is recognized, and then injects a
Content-Type
header with the application/vnd.swagger+json
media type. This ensures that when we
return JSON to the user, the content type accurately reflects the JSON structure we return.
You'll also notice that the above strategy uses dependency injection in order to receive the
JsonRenderer
instance; you will need to setup an appropriate factory for the Zend Framework 2
ServiceManager
so that you can receive the instance.
Finally, You will need to register your view strategy at some point. You have two options for when to register the strategy:
- During bootstrap (indicating it will register every single request, regardless of whether or not
the MVC
render
event is ever triggered). - During the MVC
render
event, at high priority.
In the first, case, the code might look like this:
namespace YourApi;
class Module
{
/* ... */
public function onBootstrap($e)
{
$app = $e->getTarget();
$services = $app->getServiceManager();
$view = $services->get('View');
$events = $view->getEventManager();
$events->attach($services->get('YourApi\ViewStrategy'));
}
}
If the render
event never triggers, however, there's no need to have the View
in scope, which is
why we recommend being more conservative about when you register your strategy:
namespace YourApi;
class Module
{
/* ... */
public function onBootstrap($e)
{
$app = $e->getTarget();
$events = $app->getEventManager();
$events->attach('render', [$this, 'onRender'], 200);
}
public function onRender($e)
{
$app = $e->getTarget();
$services = $app->getServiceManager();
$view = $services->get('View');
$events = $view->getEventManager();
$events->attach($services->get('YourApi\ViewStrategy'));
}
}
Configuring content negotiation
Next, you will need to create content negotiation rules. Content negotiation rules map a view
model to one or more media types that, when present in the Accept
request header, will result in
selection of your view model.
There are two ways to configure content negotiation:
- In the Apigility Admin UI
- In your API module's configuration
Content Negotiation in the Apigility Admin UI
You can select the "Content Negotiation" page from the top menu of the Apigility UI.
You can easly add new selector and then new view models, just click on "New selector", insert a name, click on Save and the add the view model clicking on the plus (+) button.
When you add a view model, you can specify media types following the IETF media type specifications:
Content Negotiation manual configuration
You can configure content negotiation rules manually as well. We recommend writing these to your
config/autoload/global.php
file if they are specific to your application; if they will be part of
a module you will distribute later, write them to your module's config/module.config.php
file.
Either way, the configuration schema remains the same.
You will write under the zf-content-negotiation
top-level key, in a selectors
subkey, and within
the Documentation
sub-subkey. Each element of this array is a view model name as the key, with an
array of media types as the value.
[
'zf-content-negotiation' => [
'selectors' => [
'Documentation' => [
'YourApi\RamlViewModel' => [
'application/raml+yaml',
],
],
],
],
]
The above configuration adds to the existing Documentation
content negotiation selector, and
tells it that it can cast the view model created by the service to your new view model in order to
return documentation in your custom format.
Requesting alternate documentation formats
All the API documentation formats are driven by content negotiation (using the
zf-content-negotiation module).
This means you can specify the format you wish to receive via the Accept
header.
For example, if you want to retrieve the API documentation data in JSON format you can use the following request:
GET /apigility/documentation[/api[/service]] HTTP/1.1
Accept: application/json
where [api]
is the name of the API and [service]
is the name of the REST or RPC service.
To get the same result in Swagger format, you would send the following request:
GET /apigility/documentation[/api[/service]] HTTP/1.1
Accept: application/vnd.swagger+json
And for HTML:
GET /apigility/documentation[/api[/service]] HTTP/1.1
Accept: text/html