NelmioApiDocBundle/Formatter/SwaggerFormatter.php

566 lines
17 KiB
PHP
Raw Permalink Normal View History

<?php
/*
* This file is part of the NelmioApiDocBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Formatter;
2024-10-01 23:00:23 +03:00
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
2014-08-07 12:06:04 -07:00
use Nelmio\ApiDocBundle\Swagger\ModelRegistry;
use Symfony\Component\HttpFoundation\Response;
/**
* Produces Swagger-compliant resource lists and API declarations as defined here:
* https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md
*
* This formatter produces an array. Therefore output still needs to be `json_encode`d before passing on as HTTP response.
*
* @author Bezalel Hermoso <bezalelhermoso@gmail.com>
*/
class SwaggerFormatter implements FormatterInterface
{
protected $basePath;
protected $apiVersion;
protected $swaggerVersion;
2024-10-01 15:54:04 +03:00
protected $info = [];
2024-10-01 15:54:04 +03:00
protected $typeMap = [
DataTypes::INTEGER => 'integer',
DataTypes::FLOAT => 'number',
DataTypes::STRING => 'string',
DataTypes::BOOLEAN => 'boolean',
DataTypes::FILE => 'string',
DataTypes::DATE => 'string',
DataTypes::DATETIME => 'string',
2024-10-01 15:54:04 +03:00
];
2024-10-01 15:54:04 +03:00
protected $formatMap = [
DataTypes::INTEGER => 'int32',
DataTypes::FLOAT => 'float',
DataTypes::FILE => 'byte',
DataTypes::DATE => 'date',
DataTypes::DATETIME => 'date-time',
2024-10-01 15:54:04 +03:00
];
2014-08-07 12:06:04 -07:00
/**
2024-10-01 15:54:04 +03:00
* @var ModelRegistry
2014-08-07 12:06:04 -07:00
*/
protected $modelRegistry;
public function __construct($namingStategy)
{
$this->modelRegistry = new ModelRegistry($namingStategy);
}
2014-08-04 10:58:41 -07:00
/**
* @var array
*/
2024-10-01 15:54:04 +03:00
protected $authConfig;
2014-08-04 10:58:41 -07:00
2024-10-01 15:54:04 +03:00
public function setAuthenticationConfig(array $config): void
2014-08-04 10:58:41 -07:00
{
$this->authConfig = $config;
}
/**
* Format a collection of documentation data.
*
* If resource is provided, an API declaration for that resource is produced. Otherwise, a resource listing is returned.
*
2024-10-01 15:54:04 +03:00
* @param array|ApiDoc[] $collection
* @param string|null $resource
*
* @return string|array
*/
public function format(array $collection, $resource = null)
{
2024-10-01 15:54:04 +03:00
if (null === $resource) {
return $this->produceResourceListing($collection);
} else {
return $this->produceApiDeclaration($collection, $resource);
}
}
/**
* Formats the collection into Swagger-compliant output.
*
* @return array
*/
public function produceResourceListing(array $collection)
{
2024-10-01 15:54:04 +03:00
$resourceList = [
'swaggerVersion' => (string) $this->swaggerVersion,
2024-10-01 15:54:04 +03:00
'apis' => [],
'apiVersion' => (string) $this->apiVersion,
'info' => $this->getInfo(),
'authorizations' => $this->getAuthorizations(),
2024-10-01 15:54:04 +03:00
];
$apis = &$resourceList['apis'];
foreach ($collection as $item) {
/** @var $apiDoc ApiDoc */
$apiDoc = $item['annotation'];
$resource = $item['resource'];
if (!$apiDoc->isResource()) {
continue;
}
$subPath = $this->stripBasePath($resource);
$normalizedName = $this->normalizeResourcePath($subPath);
2024-10-01 15:54:04 +03:00
$apis[] = [
'path' => '/' . $normalizedName,
'description' => $apiDoc->getResourceDescription(),
2024-10-01 15:54:04 +03:00
];
}
return $resourceList;
}
protected function getAuthorizations()
{
2024-10-01 15:54:04 +03:00
$auth = [];
2014-08-04 10:58:41 -07:00
2024-10-01 15:54:04 +03:00
if (null === $this->authConfig) {
2014-08-04 10:58:41 -07:00
return $auth;
}
$config = $this->authConfig;
2024-10-01 15:54:04 +03:00
if ('http' === $config['delivery']) {
2014-08-04 10:58:41 -07:00
return $auth;
}
2024-10-01 15:54:04 +03:00
$auth['apiKey'] = [
2014-08-04 10:58:41 -07:00
'type' => 'apiKey',
'passAs' => $config['delivery'],
'keyname' => $config['name'],
2024-10-01 15:54:04 +03:00
];
2014-08-04 10:58:41 -07:00
return $auth;
}
/**
* @return array
*/
protected function getInfo()
{
return $this->info;
}
/**
* Format documentation data for one route.
*
2024-10-01 15:54:04 +03:00
* @param ApiDoc $annotation
* return string|array
*
* @throws \BadMethodCallException
*/
2024-10-01 15:54:04 +03:00
public function formatOne(ApiDoc $annotation): void
{
throw new \BadMethodCallException(sprintf('%s does not support formatting a single ApiDoc only.', __CLASS__));
}
/**
* Formats collection to produce a Swagger-compliant API declaration for the given resource.
*
2024-10-01 15:54:04 +03:00
* @param string $resource
*
* @return array
*/
protected function produceApiDeclaration(array $collection, $resource)
{
2024-10-01 15:54:04 +03:00
$apiDeclaration = [
'swaggerVersion' => (string) $this->swaggerVersion,
'apiVersion' => (string) $this->apiVersion,
'basePath' => $this->basePath,
'resourcePath' => $resource,
2024-10-01 15:54:04 +03:00
'apis' => [],
'models' => [],
'produces' => [],
'consumes' => [],
2014-08-04 10:58:41 -07:00
'authorizations' => $this->getAuthorizations(),
2024-10-01 15:54:04 +03:00
];
$main = null;
2024-10-01 15:54:04 +03:00
$apiBag = [];
foreach ($collection as $item) {
/** @var $apiDoc ApiDoc */
$apiDoc = $item['annotation'];
$itemResource = $this->stripBasePath($item['resource']);
$input = $apiDoc->getInput();
if (!is_array($input)) {
2024-10-01 15:54:04 +03:00
$input = [
'class' => $input,
'paramType' => 'form',
2024-10-01 15:54:04 +03:00
];
} elseif (empty($input['paramType'])) {
$input['paramType'] = 'form';
}
$route = $apiDoc->getRoute();
$itemResource = $this->normalizeResourcePath($itemResource);
if ('/' . $itemResource !== $resource) {
continue;
}
$compiled = $route->compile();
$path = $this->stripBasePath($route->getPath());
if (!isset($apiBag[$path])) {
2024-10-01 15:54:04 +03:00
$apiBag[$path] = [];
}
2024-10-01 15:54:04 +03:00
$parameters = [];
$responseMessages = [];
foreach ($compiled->getPathVariables() as $paramValue) {
2024-10-01 15:54:04 +03:00
$parameter = [
'paramType' => 'path',
'name' => $paramValue,
'type' => 'string',
'required' => true,
2024-10-01 15:54:04 +03:00
];
2024-10-01 15:54:04 +03:00
if ('_format' === $paramValue && false != ($req = $route->getRequirement('_format'))) {
$parameter['enum'] = explode('|', $req);
}
$parameters[] = $parameter;
}
$data = $apiDoc->toArray();
if (isset($data['filters'])) {
$parameters = array_merge($parameters, $this->deriveQueryParameters($data['filters']));
}
if (isset($data['parameters'])) {
2014-08-07 12:06:04 -07:00
$parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], $input['paramType']));
}
$responseMap = $apiDoc->getParsedResponseMap();
2024-10-01 15:54:04 +03:00
$statusMessages = $data['statusCodes'] ?? [];
foreach ($responseMap as $statusCode => $prop) {
if (isset($statusMessages[$statusCode])) {
$message = is_array($statusMessages[$statusCode]) ? implode('; ', $statusMessages[$statusCode]) : $statusCode[$statusCode];
} else {
$message = sprintf('See standard HTTP status code reason for %s', $statusCode);
}
$className = !empty($prop['type']['form_errors']) ? $prop['type']['class'] . '.ErrorResponse' : $prop['type']['class'];
2024-10-01 15:54:04 +03:00
if (isset($prop['type']['collection']) && true === $prop['type']['collection']) {
/*
* Without alias: Fully\Qualified\Class\Name[]
* With alias: Fully\Qualified\Class\Name[alias]
*/
$alias = $prop['type']['collectionName'];
$newName = sprintf('%s[%s]', $className, $alias);
$collId =
$this->registerModel(
$newName,
2024-10-01 15:54:04 +03:00
[
$alias => [
'dataType' => null,
'subType' => $className,
'actualType' => DataTypes::COLLECTION,
'required' => true,
'readonly' => true,
'description' => null,
2024-10-01 15:54:04 +03:00
'default' => null,
'children' => $prop['model'][$alias]['children'],
],
],
''
);
2024-10-01 15:54:04 +03:00
$responseModel = [
'code' => $statusCode,
'message' => $message,
2024-10-01 15:54:04 +03:00
'responseModel' => $collId,
];
} else {
2024-10-01 15:54:04 +03:00
$responseModel = [
'code' => $statusCode,
'message' => $message,
'responseModel' => $this->registerModel($className, $prop['model'], ''),
2024-10-01 15:54:04 +03:00
];
}
$responseMessages[$statusCode] = $responseModel;
}
$unmappedMessages = array_diff(array_keys($statusMessages), array_keys($responseMessages));
foreach ($unmappedMessages as $code) {
2024-10-01 15:54:04 +03:00
$responseMessages[$code] = [
'code' => $code,
'message' => is_array($statusMessages[$code]) ? implode('; ', $statusMessages[$code]) : $statusMessages[$code],
2024-10-01 15:54:04 +03:00
];
}
2024-10-01 15:54:04 +03:00
$type = $responseMessages[200]['responseModel'] ?? null;
foreach ($apiDoc->getRoute()->getMethods() as $method) {
2024-10-01 15:54:04 +03:00
$operation = [
'method' => $method,
'summary' => $apiDoc->getDescription(),
'nickname' => $this->generateNickname($method, $itemResource),
'parameters' => $parameters,
'responseMessages' => array_values($responseMessages),
2024-10-01 15:54:04 +03:00
];
2024-10-01 15:54:04 +03:00
if (null !== $type) {
$operation['type'] = $type;
}
$apiBag[$path][] = $operation;
}
}
$apiDeclaration['resourcePath'] = $resource;
foreach ($apiBag as $path => $operations) {
2024-10-01 15:54:04 +03:00
$apiDeclaration['apis'][] = [
'path' => $path,
'operations' => $operations,
2024-10-01 15:54:04 +03:00
];
}
2014-08-07 12:06:04 -07:00
$apiDeclaration['models'] = $this->modelRegistry->getModels();
2014-08-07 12:23:55 -07:00
$this->modelRegistry->clear();
return $apiDeclaration;
}
/**
* Slugify a URL path. Trims out path parameters wrapped in curly brackets.
*
* @return string
*/
protected function normalizeResourcePath($path)
{
$path = preg_replace('/({.*?})/', '', $path);
$path = trim(preg_replace('/[^0-9a-zA-Z]/', '-', $path), '-');
$path = preg_replace('/-+/', '-', $path);
2015-03-06 11:19:08 +01:00
return $path;
}
2024-10-01 15:54:04 +03:00
public function setBasePath($path): void
{
$this->basePath = $path;
}
/**
* Formats query parameters to Swagger-compliant form.
*
* @return array
*/
protected function deriveQueryParameters(array $input)
{
2024-10-01 15:54:04 +03:00
$parameters = [];
foreach ($input as $name => $prop) {
if (!isset($prop['dataType'])) {
$prop['dataType'] = 'string';
}
2024-10-01 15:54:04 +03:00
$parameters[] = [
'paramType' => 'query',
'name' => $name,
2024-10-01 15:54:04 +03:00
'type' => $this->typeMap[$prop['dataType']] ?? 'string',
'description' => $prop['description'] ?? null,
];
}
return $parameters;
}
/**
* Builds a Swagger-compliant parameter list from the provided parameter array. Models are built when necessary.
*
* @param string $paramType
*
* @return array
*/
2014-08-07 12:06:04 -07:00
protected function deriveParameters(array $input, $paramType = 'form')
{
2024-10-01 15:54:04 +03:00
$parameters = [];
foreach ($input as $name => $prop) {
$type = null;
$format = null;
$ref = null;
$enum = null;
$items = null;
if (!isset($prop['actualType'])) {
$prop['actualType'] = 'string';
}
2024-10-01 15:54:04 +03:00
if (isset($this->typeMap[$prop['actualType']])) {
$type = $this->typeMap[$prop['actualType']];
} else {
switch ($prop['actualType']) {
case DataTypes::ENUM:
$type = 'string';
2014-06-25 14:23:19 -07:00
if (isset($prop['format'])) {
$enum = explode('|', rtrim(ltrim($prop['format'], '['), ']'));
2014-06-25 14:23:19 -07:00
}
break;
case DataTypes::MODEL:
$ref =
$this->registerModel(
$prop['subType'],
2024-10-01 15:54:04 +03:00
$prop['children'] ?? null,
2014-08-07 12:06:04 -07:00
$prop['description'] ?: $prop['dataType']
2014-07-25 17:31:24 -07:00
);
break;
case DataTypes::COLLECTION:
$type = 'array';
2024-10-01 15:54:04 +03:00
if (null === $prop['subType']) {
$items = ['type' => 'string'];
} elseif (isset($this->typeMap[$prop['subType']])) {
2024-10-01 15:54:04 +03:00
$items = ['type' => $this->typeMap[$prop['subType']]];
} else {
2014-09-04 11:19:02 -07:00
$ref =
$this->registerModel(
$prop['subType'],
2024-10-01 15:54:04 +03:00
$prop['children'] ?? null,
2014-09-04 11:19:02 -07:00
$prop['description'] ?: $prop['dataType']
);
2024-10-01 15:54:04 +03:00
$items = [
2014-09-04 11:19:02 -07:00
'$ref' => $ref,
2024-10-01 15:54:04 +03:00
];
}
break;
}
}
if (isset($this->formatMap[$prop['actualType']])) {
$format = $this->formatMap[$prop['actualType']];
}
if (null === $type && null === $ref) {
/* `type` or `$ref` is required. Continue to next of none of these was determined. */
continue;
}
2024-10-01 15:54:04 +03:00
$parameter = [
'paramType' => $paramType,
'name' => $name,
2024-10-01 15:54:04 +03:00
];
if (null !== $type) {
$parameter['type'] = $type;
}
if (null !== $ref) {
$parameter['$ref'] = $ref;
$parameter['type'] = $ref;
}
if (null !== $format) {
$parameter['format'] = $format;
}
if (is_array($enum) && count($enum) > 0) {
$parameter['enum'] = $enum;
}
if (isset($prop['default'])) {
2014-07-25 17:31:24 -07:00
$parameter['defaultValue'] = $prop['default'];
}
if (isset($items)) {
$parameter['items'] = $items;
}
if (isset($prop['description'])) {
$parameter['description'] = $prop['description'];
}
$parameters[] = $parameter;
}
return $parameters;
}
/**
* Registers a model into the model array. Returns a unique identifier for the model to be used in `$ref` properties.
*
* @param string $description
2014-08-07 12:06:04 -07:00
*
* @internal param $models
*/
2024-10-01 15:54:04 +03:00
public function registerModel($className, ?array $parameters = null, $description = '')
{
2014-08-07 12:06:04 -07:00
return $this->modelRegistry->register($className, $parameters, $description);
}
2024-10-01 15:54:04 +03:00
public function setSwaggerVersion($swaggerVersion): void
{
$this->swaggerVersion = $swaggerVersion;
}
2024-10-01 15:54:04 +03:00
public function setApiVersion($apiVersion): void
{
$this->apiVersion = $apiVersion;
}
2024-10-01 15:54:04 +03:00
public function setInfo($info): void
{
$this->info = $info;
}
/**
* Strips the base path from a URL path.
*/
2015-04-30 15:34:19 +02:00
protected function stripBasePath($basePath)
{
if ('/' === $this->basePath) {
2015-04-30 15:34:19 +02:00
return $basePath;
}
2015-04-30 15:34:19 +02:00
$path = sprintf('#^%s#', preg_quote($this->basePath));
$subPath = preg_replace($path, '', $basePath);
2015-03-06 11:19:08 +01:00
return $subPath;
}
/**
* Generate nicknames based on support HTTP methods and the resource name.
*
* @return string
*/
protected function generateNickname($method, $resource)
{
$resource = preg_replace('#/^#', '', $resource);
$resource = $this->normalizeResourcePath($resource);
2015-03-06 11:19:08 +01:00
return sprintf('%s_%s', strtolower($method ?: ''), $resource);
}
2015-03-06 11:19:08 +01:00
}