diff --git a/Command/DumpCommand.php b/Command/DumpCommand.php
index f1b6271..c2d929f 100644
--- a/Command/DumpCommand.php
+++ b/Command/DumpCommand.php
@@ -11,9 +11,9 @@
namespace Nelmio\ApiDocBundle\Command;
-use Psr\Container\ContainerInterface;
+use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
+use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -21,16 +21,21 @@ use Symfony\Component\Console\Output\OutputInterface;
class DumpCommand extends Command
{
/**
- * @var ContainerInterface
+ * @var RenderOpenApi
*/
- private $generatorLocator;
+ private $renderOpenApi;
/**
- * DumpCommand constructor.
+ * @var mixed[]
*/
- public function __construct(ContainerInterface $generatorLocator)
+ private $defaultHtmlConfig = [
+ 'assets_mode' => AssetsMode::CDN,
+ 'swagger_ui_config' => [],
+ ];
+
+ public function __construct(RenderOpenApi $renderOpenApi)
{
- $this->generatorLocator = $generatorLocator;
+ $this->renderOpenApi = $renderOpenApi;
parent::__construct();
}
@@ -40,34 +45,48 @@ class DumpCommand extends Command
*/
protected function configure()
{
+ $availableFormats = $this->renderOpenApi->getAvailableFormats();
$this
- ->setDescription('Dumps documentation in OpenAPI JSON format')
+ ->setDescription('Dumps documentation in OpenAPI format to: '.implode(', ', $availableFormats))
->addOption('area', '', InputOption::VALUE_OPTIONAL, '', 'default')
- ->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format output')
+ ->addOption(
+ 'format',
+ '',
+ InputOption::VALUE_REQUIRED,
+ 'Output format like: '.implode(', ', $availableFormats),
+ RenderOpenApi::JSON
+ )
+ ->addOption('server-url', '', InputOption::VALUE_REQUIRED, 'URL where live api doc is served')
+ ->addOption('html-config', '', InputOption::VALUE_REQUIRED, '', json_encode($this->defaultHtmlConfig))
+ ->addOption('no-pretty', '', InputOption::VALUE_NONE, 'Do not pretty format JSON output')
;
}
/**
- * @throws InvalidArgumentException If the area to dump is not valid
- *
* @return int|void
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$area = $input->getOption('area');
+ $format = $input->getOption('format');
- if (!$this->generatorLocator->has($area)) {
- throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area));
+ $options = [];
+ if (RenderOpenApi::HTML === $format) {
+ $rawHtmlConfig = json_decode($input->getOption('html-config'), true);
+ $options = is_array($rawHtmlConfig) ? $rawHtmlConfig : $this->defaultHtmlConfig;
+ } elseif (RenderOpenApi::JSON === $format) {
+ $options = [
+ 'no-pretty' => $input->hasParameterOption(['--no-pretty']),
+ ];
}
- $spec = $this->generatorLocator->get($area)->generate();
-
- if ($input->hasParameterOption(['--no-pretty'])) {
- $output->writeln(json_encode($spec));
- } else {
- $output->writeln(json_encode($spec, JSON_PRETTY_PRINT));
+ if ($input->getOption('server-url')) {
+ $options['server_url'] = $input->getOption('server-url');
}
+ $docs = $this->renderOpenApi->render($format, $area, $options);
+ $output->writeln($docs, OutputInterface::OUTPUT_RAW);
+
return 0;
}
}
diff --git a/Controller/DocumentationController.php b/Controller/DocumentationController.php
index 0450d3a..020451a 100644
--- a/Controller/DocumentationController.php
+++ b/Controller/DocumentationController.php
@@ -11,35 +11,31 @@
namespace Nelmio\ApiDocBundle\Controller;
-use OpenApi\Annotations\OpenApi;
-use OpenApi\Annotations\Server;
-use Psr\Container\ContainerInterface;
+use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final class DocumentationController
{
- private $generatorLocator;
+ /**
+ * @var RenderOpenApi
+ */
+ private $renderOpenApi;
- public function __construct(ContainerInterface $generatorLocator)
+ public function __construct(RenderOpenApi $renderOpenApi)
{
- $this->generatorLocator = $generatorLocator;
+ $this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
- if (!$this->generatorLocator->has($area)) {
+ try {
+ return JsonResponse::fromJsonString(
+ $this->renderOpenApi->renderFromRequest($request, RenderOpenApi::JSON, $area)
+ );
+ } catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}
-
- /** @var OpenApi $spec */
- $spec = $this->generatorLocator->get($area)->generate();
-
- if ('' !== $request->getBaseUrl()) {
- $spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
- }
-
- return new JsonResponse($spec);
}
}
diff --git a/Controller/SwaggerUiController.php b/Controller/SwaggerUiController.php
index 2b3d962..c5a3201 100644
--- a/Controller/SwaggerUiController.php
+++ b/Controller/SwaggerUiController.php
@@ -11,33 +11,38 @@
namespace Nelmio\ApiDocBundle\Controller;
-use OpenApi\Annotations\OpenApi;
-use OpenApi\Annotations\Server;
-use Psr\Container\ContainerInterface;
+use InvalidArgumentException;
+use Nelmio\ApiDocBundle\Render\Html\AssetsMode;
+use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Twig\Environment;
final class SwaggerUiController
{
- private $generatorLocator;
+ /**
+ * @var RenderOpenApi
+ */
+ private $renderOpenApi;
- private $twig;
-
- public function __construct(ContainerInterface $generatorLocator, $twig)
+ public function __construct(RenderOpenApi $renderOpenApi)
{
- if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) {
- throw new \InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig)));
- }
-
- $this->generatorLocator = $generatorLocator;
- $this->twig = $twig;
+ $this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
- if (!$this->generatorLocator->has($area)) {
+ try {
+ $response = new Response(
+ $this->renderOpenApi->renderFromRequest($request, RenderOpenApi::HTML, $area, [
+ 'assets_mode' => AssetsMode::BUNDLE,
+ ]),
+ Response::HTTP_OK,
+ ['Content-Type' => 'text/html']
+ );
+
+ return $response->setCharset('UTF-8');
+ } catch (InvalidArgumentException $e) {
$advice = '';
if (false !== strpos($area, '.json')) {
$advice = ' Since the area provided contains `.json`, the issue is likely caused by route priorities. Try switching the Swagger UI / the json documentation routes order.';
@@ -45,23 +50,5 @@ final class SwaggerUiController
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.%s', $area, $advice));
}
-
- /** @var OpenApi $spec */
- $spec = $this->generatorLocator->get($area)->generate();
-
- if ('' !== $request->getBaseUrl()) {
- $spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
- }
-
- return new Response(
- $this->twig->render(
- '@NelmioApiDoc/SwaggerUi/index.html.twig',
- ['swagger_data' => ['spec' => json_decode($spec->toJson(), true)]]
- ),
- Response::HTTP_OK,
- ['Content-Type' => 'text/html']
- );
-
- return $response->setCharset('UTF-8');
}
}
diff --git a/Controller/YamlDocumentationController.php b/Controller/YamlDocumentationController.php
index 7557fc8..a935a2d 100644
--- a/Controller/YamlDocumentationController.php
+++ b/Controller/YamlDocumentationController.php
@@ -11,37 +11,36 @@
namespace Nelmio\ApiDocBundle\Controller;
-use OpenApi\Annotations\OpenApi;
-use OpenApi\Annotations\Server;
-use Psr\Container\ContainerInterface;
+use InvalidArgumentException;
+use Nelmio\ApiDocBundle\Render\RenderOpenApi;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
final class YamlDocumentationController
{
- private $generatorLocator;
+ /**
+ * @var RenderOpenApi
+ */
+ private $renderOpenApi;
- public function __construct(ContainerInterface $generatorLocator)
+ public function __construct(RenderOpenApi $renderOpenApi)
{
- $this->generatorLocator = $generatorLocator;
+ $this->renderOpenApi = $renderOpenApi;
}
public function __invoke(Request $request, $area = 'default')
{
- if (!$this->generatorLocator->has($area)) {
+ try {
+ $response = new Response(
+ $this->renderOpenApi->renderFromRequest($request, RenderOpenApi::YAML, $area),
+ Response::HTTP_OK,
+ ['Content-Type' => 'text/x-yaml']
+ );
+
+ return $response->setCharset('UTF-8');
+ } catch (InvalidArgumentException $e) {
throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area));
}
-
- /** @var OpenApi $spec */
- $spec = $this->generatorLocator->get($area)->generate();
-
- if ('' !== $request->getBaseUrl()) {
- $spec->servers = [new Server(['url' => $request->getSchemeAndHttpHost().$request->getBaseUrl()])];
- }
-
- return new Response($spec->toYaml(), 200, [
- 'Content-Type' => 'text/x-yaml',
- ]);
}
}
diff --git a/Render/Html/AssetsMode.php b/Render/Html/AssetsMode.php
new file mode 100644
index 0000000..2bbdf5a
--- /dev/null
+++ b/Render/Html/AssetsMode.php
@@ -0,0 +1,19 @@
+assetExtension = $assetExtension;
+ $this->cdnUrl = 'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle/Resources/public';
+ $this->resourcesDir = __DIR__.'/../../Resources/public';
+ }
+
+ public function toTwigFunction($assetsMode): TwigFunction
+ {
+ $this->assetsMode = $assetsMode;
+
+ return new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]);
+ }
+
+ public function __invoke($asset)
+ {
+ [$extension, $mode] = $this->getExtension($asset);
+ [$resource, $isInline] = $this->getResource($asset, $mode);
+ if ('js' == $extension) {
+ return $this->renderJavascript($resource, $isInline);
+ } elseif ('css' == $extension) {
+ return $this->renderCss($resource, $isInline);
+ } else {
+ return $resource;
+ }
+ }
+
+ private function getExtension($asset)
+ {
+ $extension = mb_substr($asset, -3, 3, 'utf-8');
+ if ('.js' === $extension) {
+ return ['js', $this->assetsMode];
+ } elseif ('png' === $extension) {
+ return ['png', AssetsMode::OFFLINE == $this->assetsMode ? AssetsMode::CDN : $this->assetsMode];
+ } else {
+ return ['css', $this->assetsMode];
+ }
+ }
+
+ private function getResource($asset, $mode)
+ {
+ if (filter_var($asset, FILTER_VALIDATE_URL)) {
+ return [$asset, false];
+ } elseif (AssetsMode::OFFLINE === $mode) {
+ return [file_get_contents($this->resourcesDir.'/'.$asset), true];
+ } elseif (AssetsMode::CDN === $mode) {
+ return [$this->cdnUrl.'/'.$asset, false];
+ } else {
+ return [$this->assetExtension->getAssetUrl(sprintf('bundles/nelmioapidoc/%s', $asset)), false];
+ }
+ }
+
+ private function renderJavascript(string $script, bool $isInline)
+ {
+ if ($isInline) {
+ return sprintf('', $script);
+ } else {
+ return sprintf('', $script);
+ }
+ }
+
+ private function renderCss(string $stylesheet, bool $isInline)
+ {
+ if ($isInline) {
+ return sprintf('', $stylesheet);
+ } else {
+ return sprintf('', $stylesheet);
+ }
+ }
+}
diff --git a/Render/Html/HtmlOpenApiRenderer.php b/Render/Html/HtmlOpenApiRenderer.php
new file mode 100644
index 0000000..78577f1
--- /dev/null
+++ b/Render/Html/HtmlOpenApiRenderer.php
@@ -0,0 +1,63 @@
+twig = $twig;
+ $this->getNelmioAsset = $getNelmioAsset;
+ }
+
+ public function getFormat(): string
+ {
+ return RenderOpenApi::HTML;
+ }
+
+ public function render(OpenApi $spec, array $options = []): string
+ {
+ $options += [
+ 'assets_mode' => AssetsMode::CDN,
+ 'swagger_ui_config' => [],
+ ];
+
+ $this->twig->addFunction($this->getNelmioAsset->toTwigFunction($options['assets_mode']));
+
+ return $this->twig->render(
+ '@NelmioApiDoc/SwaggerUi/index.html.twig',
+ [
+ 'swagger_data' => ['spec' => json_decode($spec->toJson(), true)],
+ 'assets_mode' => $options['assets_mode'],
+ 'swagger_ui_config' => $options['swagger_ui_config'],
+ ]
+ );
+ }
+}
diff --git a/Render/Json/JsonOpenApiRenderer.php b/Render/Json/JsonOpenApiRenderer.php
new file mode 100644
index 0000000..62b0a7f
--- /dev/null
+++ b/Render/Json/JsonOpenApiRenderer.php
@@ -0,0 +1,37 @@
+ false,
+ ];
+ $flags = $options['no-pretty'] ? 0 : JSON_PRETTY_PRINT;
+
+ return json_encode($spec, $flags);
+ }
+}
diff --git a/Render/OpenApiRenderer.php b/Render/OpenApiRenderer.php
new file mode 100644
index 0000000..73edb66
--- /dev/null
+++ b/Render/OpenApiRenderer.php
@@ -0,0 +1,24 @@
+ */
+ private $openApiRenderers = [];
+
+ public function __construct(ContainerInterface $generatorLocator, OpenApiRenderer ...$openApiRenderers)
+ {
+ $this->generatorLocator = $generatorLocator;
+ foreach ($openApiRenderers as $openApiRenderer) {
+ $this->openApiRenderers[$openApiRenderer->getFormat()] = $openApiRenderer;
+ }
+ }
+
+ public function getAvailableFormats(): array
+ {
+ return array_keys($this->openApiRenderers);
+ }
+
+ public function renderFromRequest(Request $request, string $format, $area, array $extraOptions = [])
+ {
+ $options = [];
+ if ('' !== $request->getBaseUrl()) {
+ $options += [
+ 'server_url' => $request->getSchemeAndHttpHost().$request->getBaseUrl(),
+ ];
+ }
+ $options += $extraOptions;
+
+ return $this->render($format, $area, $options);
+ }
+
+ /**
+ * @throws InvalidArgumentException If the area to dump is not valid
+ */
+ public function render(string $format, string $area, array $options = []): string
+ {
+ if (!$this->generatorLocator->has($area)) {
+ throw new InvalidArgumentException(sprintf('Area "%s" is not supported.', $area));
+ } elseif (!array_key_exists($format, $this->openApiRenderers)) {
+ throw new InvalidArgumentException(sprintf('Format "%s" is not supported.', $format));
+ }
+
+ /** @var OpenApi $spec */
+ $spec = $this->generatorLocator->get($area)->generate();
+
+ if (array_key_exists('server_url', $options) && $options['server_url']) {
+ $spec->servers = [new Server(['url' => $options['server_url']])];
+ }
+
+ return $this->openApiRenderers[$format]->render($spec, $options);
+ }
+}
diff --git a/Render/Yaml/YamlOpenApiRenderer.php b/Render/Yaml/YamlOpenApiRenderer.php
new file mode 100644
index 0000000..b96a284
--- /dev/null
+++ b/Render/Yaml/YamlOpenApiRenderer.php
@@ -0,0 +1,32 @@
+toYaml();
+ }
+}
diff --git a/Resources/config/services.xml b/Resources/config/services.xml
index 8c888a6..3caaba4 100644
--- a/Resources/config/services.xml
+++ b/Resources/config/services.xml
@@ -6,24 +6,42 @@
-
+
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/doc/commands.rst b/Resources/doc/commands.rst
new file mode 100644
index 0000000..839a122
--- /dev/null
+++ b/Resources/doc/commands.rst
@@ -0,0 +1,45 @@
+Commands
+========
+
+A command is provided in order to dump the documentation in ``json``, ``yaml`` or ``html``.
+
+.. code-block:: bash
+
+ $ php app/console api:doc:dump [--format="..."]
+
+The ``--format`` option allows to choose the format (default is: ``json``).
+
+By default, the generated JSON will be pretty-formatted. If you want to generate a json
+without whitespace, use the ``--no-pretty`` option.
+
+.. code-block:: bash
+
+ $ php app/console api:doc:dump --format=json > json-pretty-formatted.json
+ $ php app/console api:doc:dump --format=json --no-pretty > json-no-pretty.json
+
+Every format can override API url. Useful if static documentation is not hosted on API url:
+
+.. code-block:: bash
+
+ $ php app/console api:doc:dump --format=yaml --server-url "http://example.com/api" > api.yaml
+
+For example to generate a static version of your documentation you can use:
+
+.. code-block:: bash
+
+ $ php app/console api:doc:dump --format=html > api.html
+
+By default, the generated HTML will add the sandbox feature.
+If you want to generate a static version of your documentation without sandbox,
+or configure UI configuration, use the ``--html-config`` option.
+
+- ``assets_mode`` - `cdn` loads assets from CDN, `offline` inlines assets
+- ``server_url`` - API url, useful if static documentation is not hosted on API url
+- ``swagger_ui_config`` - `configure Swagger UI`_
+ - ``"supportedSubmitMethods":[]`` disables the sandbox
+
+.. code-block:: bash
+
+ $ php app/console api:doc:dump --format=html --html-config '{"assets_mode":"offline","server_url":"https://example.com","swagger_ui_config":{"supportedSubmitMethods":[]}}' > api.html
+
+.. _`configure Swagger UI`: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst
index fdc7c0e..9daa3ea 100644
--- a/Resources/doc/index.rst
+++ b/Resources/doc/index.rst
@@ -339,6 +339,7 @@ If you need more complex features, take a look at:
areas
alternative_names
customization
+ commands
faq
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html
diff --git a/Resources/views/SwaggerUi/index.html.twig b/Resources/views/SwaggerUi/index.html.twig
index 82e1332..a1b3129 100644
--- a/Resources/views/SwaggerUi/index.html.twig
+++ b/Resources/views/SwaggerUi/index.html.twig
@@ -14,8 +14,8 @@ file that was distributed with this source code. #}
{% block title %}{{ swagger_data.spec.info.title }}{% endblock title %}
{% block stylesheets %}
-
-
+ {{ nelmioAsset('swagger-ui/swagger-ui.css') }}
+ {{ nelmioAsset('style.css') }}
{% endblock stylesheets %}
{% block swagger_data %}
@@ -53,7 +53,9 @@ file that was distributed with this source code. #}
{% endblock svg_icons %}
{% block header %}
-
+
+
+
{% endblock header %}
@@ -62,14 +64,18 @@ file that was distributed with this source code. #}
{% endblock %}
{% block javascripts %}
-
-
+ {{ nelmioAsset('swagger-ui/swagger-ui-bundle.js') }}
+ {{ nelmioAsset('swagger-ui/swagger-ui-standalone-preset.js') }}
{% endblock javascripts %}
-
+ {{ nelmioAsset('init-swagger-ui.js') }}
+
{% block swagger_initialization %}
{% endblock swagger_initialization %}