Compare commits

..

No commits in common. "master" and "2.2.2" have entirely different histories.

133 changed files with 3292 additions and 14107 deletions

View file

@ -1,47 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- "*.*"
- master
jobs:
tests:
name: PHPUnit PHP ${{ matrix.php-version }} ${{ matrix.dependency }} (Symfony ${{ matrix.symfony-version }})
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
symfony-version:
- '5.4.*'
- '6.4.*'
coverage: [ 'none' ]
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
- name: Configure Symfony
run: composer config extra.symfony.require "${{ matrix.symfony-version }}"
- name: Update project dependencies
run: composer update --no-progress --ansi --prefer-stable
- name: Validate composer
run: composer validate --strict --no-check-lock
- name: "Run code-style check"
run: vendor/bin/php-cs-fixer fix --dry-run --config=.php-cs-fixer.dist.php --using-cache=no --show-progress=none -v
- name: Run tests
run: vendor/bin/phpunit

2
.gitignore vendored
View file

@ -1,5 +1,3 @@
vendor/ vendor/
composer.lock composer.lock
phpunit.xml phpunit.xml
.idea
.phpunit.result.cache

View file

@ -1,11 +0,0 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
;
return Retailcrm\PhpCsFixer\Defaults::rules()
->setFinder($finder)
->setCacheFile(__DIR__ . '/.php_cs.cache');

11
.travis.yml Normal file
View file

@ -0,0 +1,11 @@
language: php
php:
- 5.3
- 5.4
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install --dev
script: phpunit

466
Annotation/ApiDoc.php Normal file
View file

@ -0,0 +1,466 @@
<?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\Annotation;
use Symfony\Component\Routing\Route;
/**
* @Annotation
*/
class ApiDoc
{
/**
* Requirements are mandatory parameters in a route.
*
* @var array
*/
private $requirements = array();
/**
* Filters are optional parameters in the query string.
*
* @var array
*/
private $filters = array();
/**
* Parameters are data a client can send.
*
* @var array
*/
private $parameters = array();
/**
* @var string
*/
private $input = null;
/**
* @var string
*/
private $output = null;
/**
* Most of the time, a single line of text describing the action.
*
* @var string
*/
private $description = null;
/**
* Section to group actions together.
*
* @var string
*/
private $section = null;
/**
* Extended documentation.
*
* @var string
*/
private $documentation = null;
/**
* @var Boolean
*/
private $isResource = false;
/**
* @var string
*/
private $method;
/**
* @var string
*/
private $host;
/**
* @var string
*/
private $uri;
/**
* @var array
*/
private $response = array();
/**
* @var Route
*/
private $route;
/**
* @var boolean
*/
private $https = false;
/**
* @var boolean
*/
private $authentication = false;
/**
* @var int
*/
private $cache;
/**
* @var boolean
*/
private $deprecated = false;
/**
* @var array
*/
private $statusCodes = array();
public function __construct(array $data)
{
$this->isResource = isset($data['resource']) && $data['resource'];
if (isset($data['description'])) {
$this->description = $data['description'];
}
if (isset($data['input'])) {
$this->input = $data['input'];
} elseif (isset($data['filters'])) {
foreach ($data['filters'] as $filter) {
if (!isset($filter['name'])) {
throw new \InvalidArgumentException('A "filter" element has to contain a "name" attribute');
}
$name = $filter['name'];
unset($filter['name']);
$this->addFilter($name, $filter);
}
}
if (isset($data['output'])) {
$this->output = $data['output'];
}
if (isset($data['statusCodes'])) {
foreach ($data['statusCodes'] as $statusCode => $description) {
$this->addStatusCode($statusCode, $description);
}
}
if (isset($data['authentication'])) {
$this->setAuthentication((bool) $data['authentication']);
}
if (isset($data['cache'])) {
$this->setCache($data['cache']);
}
if (isset($data['section'])) {
$this->section = $data['section'];
}
if (isset($data['deprecated'])) {
$this->deprecated = $data['deprecated'];
}
}
/**
* @param string $name
* @param array $filter
*/
public function addFilter($name, array $filter)
{
$this->filters[$name] = $filter;
}
/**
* @param string $statusCode
* @param mixed $description
*/
public function addStatusCode($statusCode, $description)
{
$this->statusCodes[$statusCode] = !is_array($description) ? array($description) : $description;
}
/**
* @param string $name
* @param array $requirement
*/
public function addRequirement($name, array $requirement)
{
$this->requirements[$name] = $requirement;
}
/**
* @param array $requirements
*/
public function setRequirements(array $requirements)
{
$this->requirements = array_merge($this->requirements, $requirements);
}
/**
* @return string|null
*/
public function getInput()
{
return $this->input;
}
/**
* @return string|null
*/
public function getOutput()
{
return $this->output;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* @param string $section
*/
public function setSection($section)
{
$this->section = $section;
}
/**
* @return string
*/
public function getSection()
{
return $this->section;
}
/**
* @param string $documentation
*/
public function setDocumentation($documentation)
{
$this->documentation = $documentation;
}
/**
* @return Boolean
*/
public function isResource()
{
return $this->isResource;
}
/**
* @param string $name
* @param array $parameter
*/
public function addParameter($name, array $parameter)
{
$this->parameters[$name] = $parameter;
}
/**
* @param array $parameters
*/
public function setParameters(array $parameters)
{
$this->parameters = $parameters;
}
/**
* Sets the responsed data as processed by the parsers - same format as parameters
*
* @param array $response
*/
public function setResponse(array $response)
{
$this->response = $response;
}
/**
* @param Route $route
*/
public function setRoute(Route $route)
{
$this->route = $route;
if (method_exists($route, 'getHost')) {
$this->host = $route->getHost() ? : null;
} else {
$this->host = null;
}
$this->uri = $route->getPattern();
$this->method = $route->getRequirement('_method') ?: 'ANY';
}
/**
* @return Route
*/
public function getRoute()
{
return $this->route;
}
/**
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* @param string $host
*/
public function setHost($host)
{
$this->host = $host;
}
/**
* @return boolean
*/
public function getHttps()
{
return $this->https;
}
/**
* @param boolean $https
*/
public function setHttps($https)
{
$this->https = $https;
}
/**
* @return boolean
*/
public function getAuthentication()
{
return $this->authentication;
}
/**
* @param boolean $authentication
*/
public function setAuthentication($authentication)
{
$this->authentication = $authentication;
}
/**
* @return int
*/
public function getCache()
{
return $this->cache;
}
/**
* @param int $cache
*/
public function setCache($cache)
{
$this->cache = (int) $cache;
}
/**
* @return boolean
*/
public function getDeprecated()
{
return $this->deprecated;
}
/**
* @param boolean $deprecated
*/
public function setDeprecated($deprecated)
{
$this->deprecated = (bool) $deprecated;
return $this;
}
/**
* @return array
*/
public function toArray()
{
$data = array(
'method' => $this->method,
'uri' => $this->uri,
);
if ($host = $this->host) {
$data['host'] = $host;
}
if ($description = $this->description) {
$data['description'] = $description;
}
if ($documentation = $this->documentation) {
$data['documentation'] = $documentation;
}
if ($filters = $this->filters) {
$data['filters'] = $filters;
}
if ($parameters = $this->parameters) {
$data['parameters'] = $parameters;
}
if ($requirements = $this->requirements) {
$data['requirements'] = $requirements;
}
if ($response = $this->response) {
$data['response'] = $response;
}
if ($statusCodes = $this->statusCodes) {
$data['statusCodes'] = $statusCodes;
}
if ($section = $this->section) {
$data['section'] = $section;
}
if ($cache = $this->cache) {
$data['cache'] = $cache;
}
$data['https'] = $this->https;
$data['authentication'] = $this->authentication;
$data['deprecated'] = $this->deprecated;
return $data;
}
}

View file

@ -1,500 +0,0 @@
<?php
namespace Nelmio\ApiDocBundle\Attribute;
use Symfony\Component\Routing\Route;
#[\Attribute(\Attribute::TARGET_METHOD)]
class ApiDoc
{
public const DEFAULT_VIEW = 'default';
/**
* Requirements are mandatory parameters in a route.
*
* @var array<string, array<string, string>>
*/
private array $requirements = [];
/**
* Which views is this route used. Defaults to "Default"
*
* @var string[]
*/
private array $views = [];
/**
* Filters are optional parameters in the query string.
*
* @var array<string, array<string, string>>
*/
private array $filters = [];
/**
* Parameters are data a client can send.
*
* @var array<string, array<string, mixed>>
*/
private array $parameters = [];
/**
* Headers that client can send.
*
* @var array<string, array<string, mixed>>
*/
private array $headers = [];
private ?string $link = null;
/**
* Extended documentation.
*/
private ?string $documentation = null;
private Route $route;
private ?string $host = null;
private string $method;
private string $uri;
private array $response = [];
/**
* @var array<int|string, string[]>
*/
private array $statusCodes = [];
/**
* @var array<int, array<mixed>>
*/
private array $responseMap = [];
private array $parsedResponseMap = [];
/**
* @var array<string|int, string>
*/
private array $tags = [];
private ?string $scope = null;
/**
* @param string[]|string|null $description
*/
public function __construct(
private string|bool $resource = false,
private array|string|null $description = null,
private string|array|null $input = null,
private ?array $inputs = null,
private string|array|null $output = null,
private ?string $section = null,
private bool $deprecated = false,
private ?string $resourceDescription = null,
?array $filters = null,
?array $requirements = null,
array|string|null $views = null,
?array $parameters = null,
?array $headers = null,
?array $statusCodes = null,
array|string|int|null $tags = null,
?array $responseMap = null,
) {
if (null !== $filters) {
foreach ($filters as $filter) {
if (!isset($filter['name'])) {
throw new \InvalidArgumentException('A "filter" element has to contain a "name" attribute');
}
$name = $filter['name'];
unset($filter['name']);
$this->addFilter($name, $filter);
}
}
if (null !== $requirements) {
foreach ($requirements as $requirement) {
if (!isset($requirement['name'])) {
throw new \InvalidArgumentException('A "requirement" element has to contain a "name" attribute');
}
$name = $requirement['name'];
unset($requirement['name']);
$this->addRequirement($name, $requirement);
}
}
if (null !== $views) {
if (!is_array($views)) {
$views = [$views];
}
foreach ($views as $view) {
$this->addView($view);
}
}
if (null !== $parameters) {
foreach ($parameters as $parameter) {
if (!isset($parameter['name'])) {
throw new \InvalidArgumentException('A "parameter" element has to contain a "name" attribute');
}
if (!isset($parameter['dataType'])) {
throw new \InvalidArgumentException(sprintf(
'"%s" parameter element has to contain a "dataType" attribute',
$parameter['name']
));
}
$name = $parameter['name'];
unset($parameter['name']);
$this->addParameter($name, $parameter);
}
}
if (null !== $headers) {
foreach ($headers as $header) {
if (!isset($header['name'])) {
throw new \InvalidArgumentException('A "header" element has to contain a "name" attribute');
}
$name = $header['name'];
unset($header['name']);
$this->addHeader($name, $header);
}
}
if (null !== $statusCodes) {
foreach ($statusCodes as $statusCode => $statusDescription) {
$this->addStatusCode($statusCode, $statusDescription);
}
}
if (null !== $tags) {
if (is_array($tags)) {
foreach ($tags as $tag => $colorCode) {
if (is_numeric($tag)) {
$this->addTag($colorCode);
} else {
$this->addTag($tag, $colorCode);
}
}
} else {
$this->tags[] = $tags;
}
}
if (null !== $responseMap) {
$this->responseMap = $responseMap;
if (isset($this->responseMap[200])) {
$this->output = $this->responseMap[200];
}
}
}
public function addFilter(string $name, array $filter): void
{
$this->filters[$name] = $filter;
}
public function addStatusCode(int|string $statusCode, string|array $description): void
{
$this->statusCodes[$statusCode] = !is_array($description) ? [$description] : $description;
}
public function addTag(int|string $tag, string $colorCode = '#d9534f'): void
{
$this->tags[$tag] = $colorCode;
}
public function addRequirement(string $name, array $requirement): void
{
$this->requirements[$name] = $requirement;
}
public function setRequirements(array $requirements): void
{
$this->requirements = array_merge($this->requirements, $requirements);
}
public function getInput(): string|array|null
{
return $this->input;
}
public function getInputs(): ?array
{
return $this->inputs;
}
public function getOutput(): array|string|null
{
return $this->output;
}
/**
* @return string[]|string|null
*/
public function getDescription(): array|string|null
{
return $this->description;
}
/**
* @param string[]|string|null $description
*/
public function setDescription(array|string|null $description): void
{
$this->description = $description;
}
public function setLink(?string $link): void
{
$this->link = $link;
}
public function getSection(): ?string
{
return $this->section;
}
public function addView(string $view): void
{
$this->views[] = $view;
}
/**
* @return string[]
*/
public function getViews(): array
{
return $this->views;
}
public function setDocumentation(?string $documentation): void
{
$this->documentation = $documentation;
}
public function getDocumentation(): ?string
{
return $this->documentation;
}
public function isResource(): bool
{
return (bool) $this->resource;
}
public function getResource(): string|bool
{
return $this->resource && is_string($this->resource) ? $this->resource : false;
}
public function addParameter(string $name, array $parameter): void
{
$this->parameters[$name] = $parameter;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function addHeader($name, array $header): void
{
$this->headers[$name] = $header;
}
/**
* Sets the response data as processed by the parsers - same format as parameters
*/
public function setResponse(array $response): void
{
$this->response = $response;
}
public function setRoute(Route $route): void
{
$this->route = $route;
if (method_exists($route, 'getHost')) {
$this->host = $route->getHost() ?: null;
// replace route placeholders
foreach ($route->getDefaults() as $key => $value) {
if (null !== $this->host && is_string($value)) {
$this->host = str_replace('{' . $key . '}', $value, $this->host);
}
}
} else {
$this->host = null;
}
$this->uri = $route->getPath();
$this->method = $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY';
}
public function getRoute(): Route
{
return $this->route;
}
public function getHost(): ?string
{
return $this->host;
}
public function getDeprecated(): bool
{
return $this->deprecated;
}
/**
* @return array<string, array<string, string>>
*/
public function getFilters(): array
{
return $this->filters;
}
public function getRequirements(): array
{
return $this->requirements;
}
public function getParameters(): array
{
return $this->parameters;
}
public function getHeaders(): array
{
return $this->headers;
}
public function setDeprecated(bool $deprecated): void
{
$this->deprecated = $deprecated;
}
public function getMethod(): string
{
return $this->method;
}
public function setScope(string $scope): void
{
$this->scope = $scope;
}
public function getScope(): ?string
{
return $this->scope;
}
/**
* @return array
*/
public function toArray()
{
$data = [
'method' => $this->method ?? null,
'uri' => $this->uri ?? null,
];
if ($host = $this->host) {
$data['host'] = $host;
}
if ($description = $this->description) {
$data['description'] = $description;
}
if ($link = $this->link) {
$data['link'] = $link;
}
if ($documentation = $this->documentation) {
$data['documentation'] = $documentation;
}
if ($filters = $this->filters) {
$data['filters'] = $filters;
}
if ($parameters = $this->parameters) {
$data['parameters'] = $parameters;
}
if ($headers = $this->headers) {
$data['headers'] = $headers;
}
if ($requirements = $this->requirements) {
$data['requirements'] = $requirements;
}
if ($views = $this->views) {
$data['views'] = $views;
}
if ($response = $this->response) {
$data['response'] = $response;
}
if ($parsedResponseMap = $this->parsedResponseMap) {
$data['parsedResponseMap'] = $parsedResponseMap;
}
if ($statusCodes = $this->statusCodes) {
$data['statusCodes'] = $statusCodes;
}
if ($section = $this->section) {
$data['section'] = $section;
}
if ($tags = $this->tags) {
$data['tags'] = $tags;
}
if ($resourceDescription = $this->resourceDescription) {
$data['resourceDescription'] = $resourceDescription;
}
$data['deprecated'] = $this->deprecated;
$data['scope'] = $this->scope;
return $data;
}
public function getResourceDescription(): ?string
{
return $this->resourceDescription;
}
public function getResponseMap(): array
{
if (!isset($this->responseMap[200]) && null !== $this->output) {
$this->responseMap[200] = $this->output;
}
return $this->responseMap;
}
public function getParsedResponseMap(): array
{
return $this->parsedResponseMap;
}
public function setResponseForStatusCode(array $model, array $type, int $statusCode = 200): void
{
$this->parsedResponseMap[$statusCode] = ['type' => $type, 'model' => $model];
if (200 === $statusCode && $this->response !== $model) {
$this->response = $model;
}
}
}

View file

@ -1,33 +0,0 @@
Contributing
============
First of all, **thank you** for contributing, **you are awesome**!
Here are a few rules to follow in order to ease code reviews, and discussions before
maintainers accept and merge your work.
You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
[PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
tool](http://cs.sensiolabs.org/).
You MUST run the test suite.
You MUST write (or update) unit tests.
You SHOULD write documentation.
Please, write [commit messages that make
sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
before submitting your Pull Request.
One may ask you to [squash your
commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
too. This is used to "clean" your Pull Request before merging it (we don't want
commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
Also, while creating your Pull Request on GitHub, you MUST write a description
which gives the context and/or explains why you are creating it.
Thank you!

View file

@ -11,91 +11,58 @@
namespace Nelmio\ApiDocBundle\Command; namespace Nelmio\ApiDocBundle\Command;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\HtmlFormatter;
use Nelmio\ApiDocBundle\Formatter\MarkdownFormatter;
use Nelmio\ApiDocBundle\Formatter\SimpleFormatter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsCommand( class DumpCommand extends ContainerAwareCommand
name: 'api:doc:dump',
description: 'Dumps API documentation in various formats',
)]
class DumpCommand extends Command
{ {
private const AVAILABLE_FORMATS = ['markdown', 'json', 'html'];
/** /**
* @param TranslatorInterface&LocaleAwareInterface $translator * @var array
*/ */
public function __construct( protected $availableFormats = array('markdown', 'json', 'html');
private readonly SimpleFormatter $simpleFormatter,
private readonly MarkdownFormatter $markdownFormatter,
private readonly HtmlFormatter $htmlFormatter,
private readonly ApiDocExtractor $apiDocExtractor,
private readonly TranslatorInterface $translator,
) {
parent::__construct();
}
protected function configure(): void protected function configure()
{ {
$this $this
->setDescription('')
->addOption( ->addOption(
'format', '', InputOption::VALUE_REQUIRED, 'format', '', InputOption::VALUE_REQUIRED,
'Output format like: ' . implode(', ', self::AVAILABLE_FORMATS), 'Output format like: ' . implode(', ', $this->availableFormats),
self::AVAILABLE_FORMATS[0] $this->availableFormats[0]
) )
->addOption('api-version', null, InputOption::VALUE_REQUIRED, 'The API version')
->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale for translation')
->addOption('view', '', InputOption::VALUE_OPTIONAL, '', ApiDoc::DEFAULT_VIEW)
->addOption('no-sandbox', '', InputOption::VALUE_NONE) ->addOption('no-sandbox', '', InputOption::VALUE_NONE)
; ->setName('api:doc:dump')
;
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output)
{ {
$format = $input->getOption('format'); $format = $input->getOption('format');
$view = $input->getOption('view'); $routeCollection = $this->getContainer()->get('router')->getRouteCollection();
$formatter = match ($format) { if (!$input->hasOption('format') || in_array($format, array('json'))) {
'json' => $this->simpleFormatter, $formatter = $this->getContainer()->get('nelmio_api_doc.formatter.simple_formatter');
'markdown' => $this->markdownFormatter, } else {
'html' => $this->htmlFormatter, if (!in_array($format, $this->availableFormats)) {
default => throw new \RuntimeException(sprintf('Format "%s" not supported.', $format)), throw new \RuntimeException(sprintf('Format "%s" not supported.', $format));
}; }
if ($input->hasOption('locale')) { $formatter = $this->getContainer()->get(sprintf('nelmio_api_doc.formatter.%s_formatter', $format));
$this->translator->setLocale($input->getOption('locale') ?? '');
} }
if ($input->hasOption('api-version')) { if ($input->hasOption('no-sandbox') && 'html' === $format) {
$formatter->setVersion($input->getOption('api-version'));
}
if ($formatter instanceof HtmlFormatter && $input->getOption('no-sandbox')) {
$formatter->setEnableSandbox(false); $formatter->setEnableSandbox(false);
} }
$extractedDoc = $input->hasOption('api-version') ? $extractedDoc = $this->getContainer()->get('nelmio_api_doc.extractor.api_doc_extractor')->all();
$this->apiDocExtractor->allForVersion($input->getOption('api-version'), $view) :
$this->apiDocExtractor->all($view);
$formattedDoc = $formatter->format($extractedDoc); $formattedDoc = $formatter->format($extractedDoc);
if ('json' === $format) { if ('json' === $format) {
$output->writeln(json_encode($formattedDoc, JSON_THROW_ON_ERROR)); $output->writeln(json_encode($formattedDoc));
} else { } else {
$output->writeln($formattedDoc, OutputInterface::OUTPUT_RAW); $output->writeln($formattedDoc);
} }
return 0;
} }
} }

View file

@ -1,164 +0,0 @@
<?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\Command;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\SwaggerFormatter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
/**
* Console command to dump Swagger-compliant API definitions.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
#[AsCommand(
name: 'api:swagger:dump',
description: 'Dumps Swagger-compliant API definitions.',
)]
class SwaggerDumpCommand extends Command
{
private Filesystem $filesystem;
public function __construct(
private readonly ApiDocExtractor $extractor,
private readonly SwaggerFormatter $formatter,
) {
parent::__construct();
}
protected function configure(): void
{
$this->filesystem = new Filesystem();
$this
->addOption('resource', 'r', InputOption::VALUE_OPTIONAL, 'A specific resource API declaration to dump.')
->addOption('list-only', 'l', InputOption::VALUE_NONE, 'Dump resource list only.')
->addOption('pretty', 'p', InputOption::VALUE_NONE, 'Dump as prettified JSON.')
->addArgument('destination', InputArgument::OPTIONAL, 'Directory to dump JSON files in.', null)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('list-only') && $input->getOption('resource')) {
throw new \RuntimeException('Cannot selectively dump a resource with the --list-only flag.');
}
$apiDocs = $this->extractor->all();
if ($input->getOption('list-only')) {
$data = $this->getResourceList($apiDocs);
$this->dump($data, null, $input, $output);
return 0;
}
if (false !== ($resource = $input->getOption('resource'))) {
$data = $this->getApiDeclaration($apiDocs, $resource);
if (0 === count($data['apis'])) {
throw new \InvalidArgumentException(sprintf('Resource "%s" does not exist.', $resource));
}
$this->dump($data, $resource, $input, $output);
return 0;
}
/*
* If --list-only and --resource is not specified, dump everything.
*/
$data = $this->getResourceList($apiDocs);
if (!$input->getArguments('destination')) {
$output->writeln('');
$output->writeln('<comment>Resource list: </comment>');
}
$this->dump($data, null, $input, $output, false);
foreach ($data['apis'] as $api) {
$resource = substr($api['path'], 1);
if (!$input->getArgument('destination')) {
$output->writeln('');
$output->writeln(sprintf('<comment>API declaration for <info>"%s"</info> resource: </comment>', $resource));
}
$data = $this->getApiDeclaration($apiDocs, $resource, $output);
$this->dump($data, $resource, $input, $output, false);
}
return 0;
}
protected function dump(array $data, $resource, InputInterface $input, OutputInterface $output, $treatAsFile = true): void
{
$destination = $input->getArgument('destination');
$content = json_encode($data, $input->getOption('pretty') ? JSON_PRETTY_PRINT : 0);
if (!$destination) {
$output->writeln($content);
return;
}
if (false === $treatAsFile) {
if (!$this->filesystem->exists($destination)) {
$this->filesystem->mkdir($destination);
}
}
if (!$resource) {
if (!$treatAsFile) {
$destination = sprintf('%s/api-docs.json', rtrim($destination, '\\/'));
}
$message = sprintf('<comment>Dumping resource list to %s: </comment>', $destination);
$this->writeToFile($content, $destination, $output, $message);
return;
}
if (false === $treatAsFile) {
$destination = sprintf('%s/%s.json', rtrim($destination, '\\/'), $resource);
}
$message = sprintf('<comment>Dump API declaration to %s: </comment>', $destination);
$this->writeToFile($content, $destination, $output, $message);
}
protected function writeToFile($content, $file, OutputInterface $output, $message): void
{
try {
$this->filesystem->dumpFile($file, $content);
$message .= ' <info>OK</info>';
} catch (IOException $e) {
$message .= sprintf(' <error>NOT OK - %s</error>', $e->getMessage());
}
$output->writeln($message);
}
protected function getResourceList(array $data)
{
return $this->formatter->format($data);
}
protected function getApiDeclaration(array $data, $resource)
{
return $this->formatter->format($data, '/' . $resource);
}
}

View file

@ -11,51 +11,16 @@
namespace Nelmio\ApiDocBundle\Controller; namespace Nelmio\ApiDocBundle\Controller;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\HtmlFormatter;
use Nelmio\ApiDocBundle\Formatter\RequestAwareSwaggerFormatter;
use Nelmio\ApiDocBundle\Formatter\SwaggerFormatter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class ApiDocController extends AbstractController class ApiDocController extends Controller
{ {
public function __construct( public function indexAction()
private readonly ApiDocExtractor $extractor,
private readonly HtmlFormatter $htmlFormatter,
private readonly SwaggerFormatter $swaggerFormatter,
) {
}
public function index(Request $request, $view = ApiDoc::DEFAULT_VIEW)
{ {
$apiVersion = $request->query->get('_version', null); $extractedDoc = $this->get('nelmio_api_doc.extractor.api_doc_extractor')->all();
$htmlContent = $this->get('nelmio_api_doc.formatter.html_formatter')->format($extractedDoc);
if ($apiVersion) { return new Response($htmlContent, 200, array('Content-Type' => 'text/html'));
$this->htmlFormatter->setVersion($apiVersion);
$extractedDoc = $this->extractor->allForVersion($apiVersion, $view);
} else {
$extractedDoc = $this->extractor->all($view);
}
$htmlContent = $this->htmlFormatter->format($extractedDoc);
return new Response($htmlContent, 200, ['Content-Type' => 'text/html']);
}
public function swagger(Request $request, $resource = null)
{
$docs = $this->extractor->all();
$formatter = new RequestAwareSwaggerFormatter($request, $this->swaggerFormatter);
$spec = $formatter->format($docs, $resource ? '/' . $resource : null);
if (null !== $resource && 0 === count($spec['apis'])) {
throw $this->createNotFoundException(sprintf('Cannot find resource "%s"', $resource));
}
return new JsonResponse($spec);
} }
} }

View file

@ -1,64 +0,0 @@
<?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;
/**
* All the supported data-types which will be specified in the `actualType` properties in parameters.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class DataTypes
{
public const INTEGER = 'integer';
public const FLOAT = 'float';
public const STRING = 'string';
public const BOOLEAN = 'boolean';
public const FILE = 'file';
public const ENUM = 'choice';
public const COLLECTION = 'collection';
public const MODEL = 'model';
public const DATE = 'date';
public const DATETIME = 'datetime';
public const TIME = 'time';
/**
* Returns true if the supplied `actualType` value is considered a primitive type. Returns false, otherwise.
*
* @param string $type
*
* @return bool
*/
public static function isPrimitive($type)
{
return in_array(strtolower($type), [
static::INTEGER,
static::FLOAT,
static::STRING,
static::BOOLEAN,
static::FILE,
static::DATE,
static::DATETIME,
static::TIME,
static::ENUM,
]);
}
}

View file

@ -16,35 +16,23 @@ use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface class Configuration implements ConfigurationInterface
{ {
public function getConfigTreeBuilder(): TreeBuilder public function getConfigTreeBuilder()
{ {
$treeBuilder = new TreeBuilder('nelmio_api_doc'); $treeBuilder = new TreeBuilder();
$treeBuilder
if (method_exists($treeBuilder, 'getRootNode')) { ->root('nelmio_api_doc')
$rootNode = $treeBuilder->getRootNode();
} else {
// symfony < 4.2 support
$rootNode = $treeBuilder->root('nelmio_api_doc');
}
$rootNode
->children() ->children()
->scalarNode('name')->defaultValue('API documentation')->end() ->scalarNode('name')->defaultValue('API documentation')->end()
->arrayNode('exclude_sections')
->prototype('scalar')
->end()
->end()
->booleanNode('default_sections_opened')->defaultTrue()->end()
->arrayNode('motd') ->arrayNode('motd')
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->children() ->children()
->scalarNode('template')->defaultValue('@NelmioApiDoc/Components/motd.html.twig')->end() ->scalarNode('template')->defaultValue('NelmioApiDocBundle::Components/motd.html.twig')->end()
->end() ->end()
->end() ->end()
->arrayNode('request_listener') ->arrayNode('request_listener')
->beforeNormalization() ->beforeNormalization()
->ifTrue(function ($a) { return is_bool($a); }) ->ifTrue(function ($a) { return is_bool($a); })
->then(function ($a) { return ['enabled' => $a]; }) ->then(function ($a) { return array('enabled' => $a); })
->end() ->end()
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->children() ->children()
@ -57,120 +45,37 @@ class Configuration implements ConfigurationInterface
->children() ->children()
->scalarNode('enabled')->defaultTrue()->end() ->scalarNode('enabled')->defaultTrue()->end()
->scalarNode('endpoint')->defaultNull()->end() ->scalarNode('endpoint')->defaultNull()->end()
->scalarNode('accept_type')->defaultNull()->end() ->scalarNode('accept_type')->defaultValue('')->end()
->arrayNode('body_format')
->addDefaultsIfNotSet()
->beforeNormalization()
->ifString()
->then(function ($v) { return ['default_format' => $v]; })
->end()
->children()
->arrayNode('formats')
->defaultValue(['form', 'json'])
->prototype('scalar')->end()
->end()
->enumNode('default_format')
->values(['form', 'json'])
->defaultValue('form')
->end()
->end()
->end()
->arrayNode('request_format') ->arrayNode('request_format')
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->children() ->children()
->arrayNode('formats')
->defaultValue([
'json' => 'application/json',
'xml' => 'application/xml',
])
->prototype('scalar')->end()
->end()
->enumNode('method') ->enumNode('method')
->values(['format_param', 'accept_header']) ->values(array('format_param', 'accept_header'))
->defaultValue('format_param') ->defaultValue('format_param')
->end() ->end()
->scalarNode('default_format')->defaultValue('json')->end() ->enumNode('default_format')
->values(array('json', 'xml'))
->defaultValue('json')
->end()
->end() ->end()
->end() ->end()
->arrayNode('authentication') ->arrayNode('authentication')
->children() ->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('delivery') ->scalarNode('delivery')
->isRequired() ->isRequired()
->validate() ->validate()
->ifNotInArray(['query', 'http', 'header']) // header|query|request, but only query is implemented for now
->ifNotInArray(array('query', 'http_basic'))
->thenInvalid("Unknown authentication delivery type '%s'.") ->thenInvalid("Unknown authentication delivery type '%s'.")
->end() ->end()
->end() ->end()
->scalarNode('name')->isRequired()->end()
->enumNode('type')
->info('Required if http delivery is selected.')
->values(['basic', 'bearer'])
->end()
->booleanNode('custom_endpoint')->defaultFalse()->end() ->booleanNode('custom_endpoint')->defaultFalse()->end()
->end() ->end()
->validate()
->ifTrue(function ($v) {
return 'http' === $v['delivery'] && !$v['type'];
})
->thenInvalid('"type" is required when using http delivery.')
->end()
// http_basic BC
->beforeNormalization()
->ifTrue(function ($v) {
return 'http_basic' === $v['delivery'];
})
->then(function ($v) {
$v['delivery'] = 'http';
$v['type'] = 'basic';
return $v;
})
->end()
->beforeNormalization()
->ifTrue(function ($v) {
return 'http' === $v['delivery'];
})
->then(function ($v) {
if ('http' === $v['delivery'] && !isset($v['name'])) {
$v['name'] = 'Authorization';
}
return $v;
})
->end()
->end()
->booleanNode('entity_to_choice')->defaultTrue()->end()
->end()
->end()
->arrayNode('swagger')
->addDefaultsIfNotSet()
->children()
->scalarNode('model_naming_strategy')->defaultValue('dot_notation')->end()
->scalarNode('api_base_path')->defaultValue('/api')->end()
->scalarNode('swagger_version')->defaultValue('1.2')->end()
->scalarNode('api_version')->defaultValue('0.1')->end()
->arrayNode('info')
->addDefaultsIfNotSet()
->children()
->scalarNode('title')->defaultValue('Symfony2')->end()
->scalarNode('description')->defaultValue('My awesome Symfony2 app!')->end()
->scalarNode('TermsOfServiceUrl')->defaultNull()->end()
->scalarNode('contact')->defaultNull()->end()
->scalarNode('license')->defaultNull()->end()
->scalarNode('licenseUrl')->defaultNull()->end()
->end()
->end() ->end()
->end() ->end()
->end() ->end()
->arrayNode('cache') ->end();
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultFalse()->end()
->scalarNode('file')->defaultValue('%kernel.cache_dir%/api-doc.cache')->end()
->end()
->end()
->end()
;
return $treeBuilder; return $treeBuilder;
} }

View file

@ -11,22 +11,27 @@
namespace Nelmio\ApiDocBundle\DependencyInjection; namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
class ExtractorHandlerCompilerPass implements CompilerPassInterface class ExtractorHandlerCompilerPass implements CompilerPassInterface
{ {
public function process(ContainerBuilder $container): void /**
* {@inheritDoc}
*/
public function process(ContainerBuilder $container)
{ {
$handlers = []; $handlers = array();
foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.handler') as $id => $attributes) { foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.handler') as $id => $attributes) {
// Adding handlers from tagged services
$handlers[] = new Reference($id); $handlers[] = new Reference($id);
} }
$definition = $container->getDefinition(
$container 'nelmio_api_doc.extractor.api_doc_extractor'
->getDefinition('nelmio_api_doc.extractor.api_doc_extractor') );
->replaceArgument(2, $handlers) $definition->replaceArgument(4, $handlers);
;
} }
} }

View file

@ -1,24 +0,0 @@
<?php
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class FormInfoParserCompilerPass implements CompilerPassInterface
{
public const TAG_NAME = 'nelmio_api_doc.extractor.form_info_parser';
public function process(ContainerBuilder $container): void
{
if (!$container->has('nelmio_api_doc.parser.form_type_parser')) {
return;
}
$formParser = $container->findDefinition('nelmio_api_doc.parser.form_type_parser');
foreach ($container->findTaggedServiceIds(self::TAG_NAME) as $id => $tags) {
$formParser->addMethodCall('addFormInfoParser', [new Reference($id)]);
}
}
}

View file

@ -1,36 +0,0 @@
<?php
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
/**
* Loads parsers to extract information from different libraries.
*
* They are only loaded when the corresponding library is installed and enabled.
*/
class LoadExtractorParsersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
// forms may not be installed/enabled, if it is, load that config as well
if ($container->hasDefinition('form.factory')) {
$loader->load('services.form.xml');
}
// validation may not be installed/enabled, if it is, load that config as well
if ($container->has('validator.mapping.class_metadata_factory')) {
$loader->load('services.validation.xml');
}
// JMS may or may not be installed, if it is, load that config as well
if ($container->hasDefinition('jms_serializer.serializer')) {
$loader->load('services.jms.xml');
}
}
}

View file

@ -11,44 +11,32 @@
namespace Nelmio\ApiDocBundle\DependencyInjection; namespace Nelmio\ApiDocBundle\DependencyInjection;
use Nelmio\ApiDocBundle\Parser\FormInfoParser; use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class NelmioApiDocExtension extends Extension class NelmioApiDocExtension extends Extension
{ {
public function load(array $configs, ContainerBuilder $container): void /**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{ {
$processor = new Processor(); $processor = new Processor();
$configuration = new Configuration(); $configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs); $config = $processor->processConfiguration($configuration, $configs);
$container->setParameter('nelmio_api_doc.motd.template', $config['motd']['template']); $container->setParameter('nelmio_api_doc.motd.template', $config['motd']['template']);
$container->setParameter('nelmio_api_doc.exclude_sections', $config['exclude_sections']);
$container->setParameter('nelmio_api_doc.default_sections_opened', $config['default_sections_opened']);
$container->setParameter('nelmio_api_doc.api_name', $config['name']); $container->setParameter('nelmio_api_doc.api_name', $config['name']);
$container->setParameter('nelmio_api_doc.sandbox.enabled', $config['sandbox']['enabled']); $container->setParameter('nelmio_api_doc.sandbox.enabled', $config['sandbox']['enabled']);
$container->setParameter('nelmio_api_doc.sandbox.endpoint', $config['sandbox']['endpoint']); $container->setParameter('nelmio_api_doc.sandbox.endpoint', $config['sandbox']['endpoint']);
$container->setParameter('nelmio_api_doc.sandbox.accept_type', $config['sandbox']['accept_type']);
$container->setParameter('nelmio_api_doc.sandbox.body_format.formats', $config['sandbox']['body_format']['formats']);
$container->setParameter('nelmio_api_doc.sandbox.body_format.default_format', $config['sandbox']['body_format']['default_format']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.method', $config['sandbox']['request_format']['method']); $container->setParameter('nelmio_api_doc.sandbox.request_format.method', $config['sandbox']['request_format']['method']);
$container->setParameter('nelmio_api_doc.sandbox.accept_type', $config['sandbox']['accept_type']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.default_format', $config['sandbox']['request_format']['default_format']); $container->setParameter('nelmio_api_doc.sandbox.request_format.default_format', $config['sandbox']['request_format']['default_format']);
$container->setParameter('nelmio_api_doc.sandbox.request_format.formats', $config['sandbox']['request_format']['formats']);
$container->setParameter('nelmio_api_doc.sandbox.entity_to_choice', $config['sandbox']['entity_to_choice']);
if (method_exists($container, 'registerForAutoconfiguration')) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$container->registerForAutoconfiguration(FormInfoParser::class)
->addTag(FormInfoParserCompilerPass::TAG_NAME)
;
}
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('formatters.xml'); $loader->load('formatters.xml');
$loader->load('services.xml'); $loader->load('services.xml');
@ -60,45 +48,5 @@ class NelmioApiDocExtension extends Extension
if (isset($config['sandbox']['authentication'])) { if (isset($config['sandbox']['authentication'])) {
$container->setParameter('nelmio_api_doc.sandbox.authentication', $config['sandbox']['authentication']); $container->setParameter('nelmio_api_doc.sandbox.authentication', $config['sandbox']['authentication']);
} }
// backwards compatibility for Symfony2.1 https://github.com/nelmio/NelmioApiDocBundle/issues/231
if (!interface_exists('\Symfony\Component\Validator\MetadataFactoryInterface')) {
$container->setParameter('nelmio_api_doc.parser.validation_parser.class', 'Nelmio\ApiDocBundle\Parser\ValidationParserLegacy');
}
$container->setParameter('nelmio_api_doc.swagger.base_path', $config['swagger']['api_base_path']);
$container->setParameter('nelmio_api_doc.swagger.swagger_version', $config['swagger']['swagger_version']);
$container->setParameter('nelmio_api_doc.swagger.api_version', $config['swagger']['api_version']);
$container->setParameter('nelmio_api_doc.swagger.info', $config['swagger']['info']);
$container->setParameter('nelmio_api_doc.swagger.model_naming_strategy', $config['swagger']['model_naming_strategy']);
if (true === $config['cache']['enabled']) {
$arguments = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor')->getArguments();
$caching = new Definition('Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor');
$arguments[] = $config['cache']['file'];
$arguments[] = '%kernel.debug%';
$caching->setArguments($arguments);
$caching->setPublic(true);
$container->setDefinition('nelmio_api_doc.extractor.api_doc_extractor', $caching);
}
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('autowired.yaml');
}
/**
* @return string
*/
public function getNamespace()
{
return 'http://nelmio.github.io/schema/dic/nelmio_api_doc';
}
/**
* @return string
*/
public function getXsdValidationBasePath()
{
return __DIR__ . '/../Resources/config/schema';
} }
} }

View file

@ -2,13 +2,13 @@
namespace Nelmio\ApiDocBundle\DependencyInjection; namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
class RegisterExtractorParsersPass implements CompilerPassInterface class RegisterExtractorParsersPass implements CompilerPassInterface
{ {
public function process(ContainerBuilder $container): void public function process(ContainerBuilder $container)
{ {
if (false === $container->hasDefinition('nelmio_api_doc.extractor.api_doc_extractor')) { if (false === $container->hasDefinition('nelmio_api_doc.extractor.api_doc_extractor')) {
return; return;
@ -16,23 +16,23 @@ class RegisterExtractorParsersPass implements CompilerPassInterface
$definition = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor'); $definition = $container->getDefinition('nelmio_api_doc.extractor.api_doc_extractor');
// find registered parsers and sort by priority //find registered parsers and sort by priority
$sortedParsers = []; $sortedParsers = array();
foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.parser') as $id => $tagAttributes) { foreach ($container->findTaggedServiceIds('nelmio_api_doc.extractor.parser') as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) { foreach ($tagAttributes as $attributes) {
$priority = $attributes['priority'] ?? 0; $priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$sortedParsers[$priority][] = $id; $sortedParsers[$priority][] = $id;
} }
} }
// add parsers if any //add parsers if any
if (!empty($sortedParsers)) { if (!empty($sortedParsers)) {
krsort($sortedParsers); krsort($sortedParsers);
$sortedParsers = call_user_func_array('array_merge', $sortedParsers); $sortedParsers = call_user_func_array('array_merge', $sortedParsers);
// add method call for each registered parsers //add method call for each registered parsers
foreach ($sortedParsers as $id) { foreach ($sortedParsers as $id) {
$definition->addMethodCall('addParser', [new Reference($id)]); $definition->addMethodCall('addParser', array(new Reference($id)));
} }
} }
} }

View file

@ -0,0 +1,21 @@
<?php
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\Config\FileLocator;
class RegisterJmsParserPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
//JMS may or may not be installed, if it is, load that config as well
if ($container->hasDefinition('jms_serializer.serializer')) {
$loader->load('services.jms.xml');
}
}
}

View file

@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Compiler pass that configures the SwaggerFormatter instance.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class SwaggerConfigCompilerPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @api
*/
public function process(ContainerBuilder $container): void
{
$formatter = $container->getDefinition('nelmio_api_doc.formatter.swagger_formatter');
$formatter->addMethodCall('setBasePath', [$container->getParameter('nelmio_api_doc.swagger.base_path')]);
$formatter->addMethodCall('setApiVersion', [$container->getParameter('nelmio_api_doc.swagger.api_version')]);
$formatter->addMethodCall('setSwaggerVersion', [$container->getParameter('nelmio_api_doc.swagger.swagger_version')]);
$formatter->addMethodCall('setInfo', [$container->getParameter('nelmio_api_doc.swagger.info')]);
$authentication = $container->getParameter('nelmio_api_doc.sandbox.authentication');
$formatter->setArguments([
$container->getParameter('nelmio_api_doc.swagger.model_naming_strategy'),
]);
if (null !== $authentication) {
$formatter->addMethodCall('setAuthenticationConfig', [$authentication]);
}
}
}

View file

@ -1,6 +0,0 @@
ARG PHP_IMAGE_TAG
FROM php:${PHP_IMAGE_TAG}-cli-alpine
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /opt/test

View file

@ -14,18 +14,19 @@ namespace Nelmio\ApiDocBundle\EventListener;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor; use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Formatter\FormatterInterface; use Nelmio\ApiDocBundle\Formatter\FormatterInterface;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener class RequestListener
{ {
/** /**
* @var ApiDocExtractor * @var \Nelmio\ApiDocBundle\Extractor\ApiDocExtractor
*/ */
protected $extractor; protected $extractor;
/** /**
* @var FormatterInterface * @var \Nelmio\ApiDocBundle\Formatter\FormatterInterface
*/ */
protected $formatter; protected $formatter;
@ -41,9 +42,12 @@ class RequestListener
$this->parameter = $parameter; $this->parameter = $parameter;
} }
public function onKernelRequest(RequestEvent $event): void /**
* {@inheritdoc}
*/
public function onKernelRequest(GetResponseEvent $event)
{ {
if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) { if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return; return;
} }
@ -54,14 +58,14 @@ class RequestListener
} }
$controller = $request->attributes->get('_controller'); $controller = $request->attributes->get('_controller');
$route = $request->attributes->get('_route'); $route = $request->attributes->get('_route');
if (null !== $annotation = $this->extractor->get($controller, $route)) { if (null !== $annotation = $this->extractor->get($controller, $route)) {
$result = $this->formatter->formatOne($annotation); $result = $this->formatter->formatOne($annotation);
$event->setResponse(new Response($result, 200, [ $event->setResponse(new Response($result, 200, array(
'Content-Type' => 'text/html', 'Content-Type' => 'text/html'
])); )));
} }
} }
} }

View file

@ -11,31 +11,56 @@
namespace Nelmio\ApiDocBundle\Extractor; namespace Nelmio\ApiDocBundle\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Doctrine\Common\Annotations\Reader;
use Nelmio\ApiDocBundle\DataTypes; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Parser\ParserInterface; use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Nelmio\ApiDocBundle\Parser\PostParserInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
class ApiDocExtractor class ApiDocExtractor
{ {
/** const ANNOTATION_CLASS = 'Nelmio\\ApiDocBundle\\Annotation\\ApiDoc';
* @var ParserInterface[]
*/
protected array $parsers = [];
/** /**
* @param HandlerInterface[] $handlers * @var ContainerInterface
* @param string[] $excludeSections
*/ */
public function __construct( protected $container;
protected RouterInterface $router,
protected DocCommentExtractor $commentExtractor, /**
protected array $handlers, * @var RouterInterface
protected array $excludeSections, */
) { protected $router;
/**
* @var Reader
*/
protected $reader;
/**
* @var DocCommentExtractor
*/
private $commentExtractor;
/**
* @var array ParserInterface
*/
protected $parsers = array();
/**
* @var array HandlerInterface
*/
protected $handlers;
public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader, DocCommentExtractor $commentExtractor, array $handlers)
{
$this->container = $container;
$this->router = $router;
$this->reader = $reader;
$this->commentExtractor = $commentExtractor;
$this->handlers = $handlers;
} }
/** /**
@ -45,39 +70,19 @@ class ApiDocExtractor
* *
* @return Route[] An array of routes * @return Route[] An array of routes
*/ */
public function getRoutes(): array public function getRoutes()
{ {
return $this->router->getRouteCollection()->all(); return $this->router->getRouteCollection()->all();
} }
/*
* Extracts annotations from all known routes
*/
public function all($view = ApiDoc::DEFAULT_VIEW): array
{
return $this->extractAnnotations($this->getRoutes(), $view);
}
/** /**
* Extracts annotations from routes for specific version * Extracts annotations from all known routes
* *
* @param string $apiVersion API version * @return array
* @param string $view
*/ */
public function allForVersion($apiVersion, $view = ApiDoc::DEFAULT_VIEW): array public function all()
{ {
$data = $this->all($view); return $this->extractAnnotations($this->getRoutes());
foreach ($data as $k => $a) {
// ignore other api version's routes
if (
$a['annotation']->getRoute()->getDefault('_version')
&& !version_compare($apiVersion ?? '', $a['annotation']->getRoute()->getDefault('_version'), '=')
) {
unset($data[$k]);
}
}
return $data;
} }
/** /**
@ -86,11 +91,13 @@ class ApiDocExtractor
* - resource * - resource
* *
* @param array $routes array of Route-objects for which the annotations should be extracted * @param array $routes array of Route-objects for which the annotations should be extracted
*
* @return array
*/ */
public function extractAnnotations(array $routes, $view = ApiDoc::DEFAULT_VIEW): array public function extractAnnotations(array $routes)
{ {
$array = []; $array = array();
$resources = []; $resources = array();
foreach ($routes as $route) { foreach ($routes as $route) {
if (!$route instanceof Route) { if (!$route instanceof Route) {
@ -98,21 +105,13 @@ class ApiDocExtractor
} }
if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) { if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) {
$annotation = $this->getMethodApiDoc($method); if ($annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS)) {
if (
$annotation && !in_array($annotation->getSection(), $this->excludeSections)
&& (in_array($view, $annotation->getViews()) || (0 === count($annotation->getViews()) && ApiDoc::DEFAULT_VIEW === $view))
) {
if ($annotation->isResource()) { if ($annotation->isResource()) {
if ($resource = $annotation->getResource()) { // remove format from routes used for resource grouping
$resources[] = $resource; $resources[] = str_replace('.{_format}', '', $route->getPattern());
} else {
// remove format from routes used for resource grouping
$resources[] = str_replace('.{_format}', '', $route->getPath() ?: '');
}
} }
$array[] = ['annotation' => $this->extractData($annotation, $route, $method)]; $array[] = array('annotation' => $this->extractData($annotation, $route, $method));
} }
} }
} }
@ -120,10 +119,10 @@ class ApiDocExtractor
rsort($resources); rsort($resources);
foreach ($array as $index => $element) { foreach ($array as $index => $element) {
$hasResource = false; $hasResource = false;
$path = $element['annotation']->getRoute()->getPath() ?: ''; $pattern = $element['annotation']->getRoute()->getPattern();
foreach ($resources as $resource) { foreach ($resources as $resource) {
if (str_starts_with($path, $resource) || $resource === $element['annotation']->getResource()) { if (0 === strpos($pattern, $resource)) {
$array[$index]['resource'] = $resource; $array[$index]['resource'] = $resource;
$hasResource = true; $hasResource = true;
@ -136,17 +135,17 @@ class ApiDocExtractor
} }
} }
$methodOrder = ['GET', 'POST', 'PUT', 'DELETE']; $methodOrder = array('GET', 'POST', 'PUT', 'DELETE');
usort($array, function ($a, $b) use ($methodOrder) { usort($array, function($a, $b) use ($methodOrder) {
if ($a['resource'] === $b['resource']) { if ($a['resource'] === $b['resource']) {
if ($a['annotation']->getRoute()->getPath() === $b['annotation']->getRoute()->getPath()) { if ($a['annotation']->getRoute()->getPattern() === $b['annotation']->getRoute()->getPattern()) {
$methodA = array_search($a['annotation']->getRoute()->getMethods(), $methodOrder); $methodA = array_search($a['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
$methodB = array_search($b['annotation']->getRoute()->getMethods(), $methodOrder); $methodB = array_search($b['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
if ($methodA === $methodB) { if ($methodA === $methodB) {
return strcmp( return strcmp(
implode('|', $a['annotation']->getRoute()->getMethods()), $a['annotation']->getRoute()->getRequirement('_method'),
implode('|', $b['annotation']->getRoute()->getMethods()) $b['annotation']->getRoute()->getRequirement('_method')
); );
} }
@ -154,8 +153,8 @@ class ApiDocExtractor
} }
return strcmp( return strcmp(
$a['annotation']->getRoute()->getPath(), $a['annotation']->getRoute()->getPattern(),
$b['annotation']->getRoute()->getPath() $b['annotation']->getRoute()->getPattern()
); );
} }
@ -169,18 +168,22 @@ class ApiDocExtractor
* Returns the ReflectionMethod for the given controller string. * Returns the ReflectionMethod for the given controller string.
* *
* @param string $controller * @param string $controller
* * @return \ReflectionMethod|null
* @return \ReflectionMethod|null
*/ */
public function getReflectionMethod($controller) public function getReflectionMethod($controller)
{ {
if (null === $controller) {
return null;
}
if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) { if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) {
$class = $matches[1]; $class = $matches[1];
$method = $matches[2]; $method = $matches[2];
} elseif (preg_match('#(.+):([\w]+)#', $controller, $matches)) {
$controller = $matches[1];
$method = $matches[2];
if ($this->container->has($controller)) {
$this->container->enterScope('request');
$this->container->set('request', new Request(), 'request');
$class = get_class($this->container->get($controller));
$this->container->leaveScope('request');
}
} }
if (isset($class) && isset($method)) { if (isset($class) && isset($method)) {
@ -197,14 +200,13 @@ class ApiDocExtractor
* Returns an ApiDoc annotation. * Returns an ApiDoc annotation.
* *
* @param string $controller * @param string $controller
* @param string $route * @param Route $route
* * @return ApiDoc|null
* @return ApiDoc|null
*/ */
public function get($controller, $route) public function get($controller, $route)
{ {
if ($method = $this->getReflectionMethod($controller)) { if ($method = $this->getReflectionMethod($controller)) {
if ($annotation = $this->getMethodApiDoc($method)) { if ($annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS)) {
if ($route = $this->router->getRouteCollection()->get($route)) { if ($route = $this->router->getRouteCollection()->get($route)) {
return $this->extractData($annotation, $route, $method); return $this->extractData($annotation, $route, $method);
} }
@ -214,20 +216,12 @@ class ApiDocExtractor
return null; return null;
} }
protected function getMethodApiDoc(\ReflectionMethod $method): ?ApiDoc
{
$attributes = $method->getAttributes(ApiDoc::class, \ReflectionAttribute::IS_INSTANCEOF);
if (!$attributes) {
return null;
}
return $attributes[0]->newInstance();
}
/** /**
* Registers a class parser to use for parsing input class metadata * Registers a class parser to use for parsing input class metadata
*
* @param ParserInterface $parser
*/ */
public function addParser(ParserInterface $parser): void public function addParser(ParserInterface $parser)
{ {
$this->parsers[] = $parser; $this->parsers[] = $parser;
} }
@ -235,6 +229,9 @@ class ApiDocExtractor
/** /**
* Returns a new ApiDoc instance with more data. * Returns a new ApiDoc instance with more data.
* *
* @param ApiDoc $annotation
* @param Route $route
* @param \ReflectionMethod $method
* @return ApiDoc * @return ApiDoc
*/ */
protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMethod $method) protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
@ -242,362 +239,130 @@ class ApiDocExtractor
// create a new annotation // create a new annotation
$annotation = clone $annotation; $annotation = clone $annotation;
// doc
$annotation->setDocumentation($this->commentExtractor->getDocCommentText($method));
// parse annotations // parse annotations
$this->parseAnnotations($annotation, $route, $method); $this->parseAnnotations($annotation, $route, $method);
// route // route
$annotation->setRoute($route); $annotation->setRoute($route);
$inputs = []; // description
if (null !== $annotation->getInputs()) { if (null === $annotation->getDescription()) {
$inputs = $annotation->getInputs(); $comments = explode("\n", $this->commentExtractor->getDocCommentText($method));
} elseif (null !== $annotation->getInput()) { // just set the first line
$inputs[] = $annotation->getInput(); $comment = trim($comments[0]);
$comment = preg_replace("#\n+#", ' ', $comment);
$comment = preg_replace('#\s+#', ' ', $comment);
$comment = preg_replace('#[_`*]+#', '', $comment);
if ('@' !== substr($comment, 0, 1)) {
$annotation->setDescription($comment);
}
} }
// doc
$annotation->setDocumentation($this->commentExtractor->getDocCommentText($method));
// input (populates 'parameters' for the formatters) // input (populates 'parameters' for the formatters)
if (count($inputs)) { if (null !== $input = $annotation->getInput()) {
$parameters = []; $parameters = array();
foreach ($inputs as $input) {
$normalizedInput = $this->normalizeClassParameter($input);
$supportedParsers = [];
foreach ($this->getParsers($normalizedInput) as $parser) {
if ($parser->supports($normalizedInput)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput));
}
}
foreach ($supportedParsers as $parser) { foreach ($this->parsers as $parser) {
if ($parser instanceof PostParserInterface) { if ($parser->supports($input)) {
$parameters = $this->mergeParameters( $parameters = $parser->parse($input);
$parameters, break;
$parser->postParse($normalizedInput, $parameters)
);
}
} }
} }
$parameters = $this->setParentClasses($parameters); if ('PUT' === $method) {
$parameters = $this->clearClasses($parameters); // All parameters are optional with PUT (update)
$parameters = $this->generateHumanReadableTypes($parameters); array_walk($parameters, function($val, $key) use (&$data) {
if ('PATCH' === $annotation->getMethod()) {
// All parameters are optional with PATCH (update)
foreach ($parameters as $key => $val) {
$parameters[$key]['required'] = false; $parameters[$key]['required'] = false;
} });
} }
// merge parameters with parameters block from ApiDoc annotation in controller method
$parameters = $this->mergeParameters($parameters, $annotation->getParameters());
$annotation->setParameters($parameters); $annotation->setParameters($parameters);
} }
// output (populates 'response' for the formatters) // output (populates 'response' for the formatters)
if (null !== $output = $annotation->getOutput()) { if (null !== $output = $annotation->getOutput()) {
$response = []; $response = array();
$supportedParsers = [];
$normalizedOutput = $this->normalizeClassParameter($output); foreach ($this->parsers as $parser) {
if ($parser->supports($output)) {
foreach ($this->getParsers($normalizedOutput) as $parser) { $response = $parser->parse($output);
if ($parser->supports($normalizedOutput)) { break;
$supportedParsers[] = $parser;
$response = $this->mergeParameters($response, $parser->parse($normalizedOutput));
} }
} }
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedOutput, $response);
$response = $this->mergeParameters($response, $mp);
}
}
$response = $this->clearClasses($response);
$response = $this->generateHumanReadableTypes($response);
$annotation->setResponse($response); $annotation->setResponse($response);
$annotation->setResponseForStatusCode($response, $normalizedOutput, 200);
} }
if (count($annotation->getResponseMap()) > 0) { // requirements
foreach ($annotation->getResponseMap() as $code => $modelName) { $requirements = array();
if ('200' === (string) $code && isset($modelName['type']) && isset($modelName['model'])) { foreach ($route->getRequirements() as $name => $value) {
/* if ('_method' !== $name) {
* Model was already parsed as the default `output` for this ApiDoc. $requirements[$name] = array(
*/ 'requirement' => $value,
continue; 'dataType' => '',
} 'description' => '',
);
$normalizedModel = $this->normalizeClassParameter($modelName); }
if ('_scheme' == $name) {
$parameters = []; $https = ('https' == $value);
$supportedParsers = []; $annotation->setHttps($https);
foreach ($this->getParsers($normalizedModel) as $parser) {
if ($parser->supports($normalizedModel)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedModel));
}
}
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedModel, $parameters);
$parameters = $this->mergeParameters($parameters, $mp);
}
}
$parameters = $this->setParentClasses($parameters);
$parameters = $this->clearClasses($parameters);
$parameters = $this->generateHumanReadableTypes($parameters);
$annotation->setResponseForStatusCode($parameters, $normalizedModel, $code);
} }
} }
$paramDocs = array();
foreach (explode("\n", $this->commentExtractor->getDocComment($method)) as $line) {
if (preg_match('{^@param (.+)}', trim($line), $matches)) {
$paramDocs[] = $matches[1];
}
if (preg_match('{^@deprecated\b(.*)}', trim($line), $matches)) {
$annotation->setDeprecated(true);
}
}
$regexp = '{(\w*) *\$%s\b *(.*)}i';
foreach ($route->compile()->getVariables() as $var) {
$found = false;
foreach ($paramDocs as $paramDoc) {
if (preg_match(sprintf($regexp, preg_quote($var)), $paramDoc, $matches)) {
$requirements[$var]['dataType'] = isset($matches[1]) ? $matches[1] : '';
$requirements[$var]['description'] = $matches[2];
if (!isset($requirements[$var]['requirement'])) {
$requirements[$var]['requirement'] = '';
}
$found = true;
break;
}
}
if (!isset($requirements[$var]) && false === $found) {
$requirements[$var] = array('requirement' => '', 'dataType' => '', 'description' => '');
}
}
$annotation->setRequirements($requirements);
return $annotation; return $annotation;
} }
protected function normalizeClassParameter($input)
{
$defaults = [
'class' => '',
'groups' => [],
'options' => [],
];
// normalize strings
if (is_string($input)) {
$input = ['class' => $input];
}
$collectionData = [];
/*
* Match array<Fully\Qualified\ClassName> as alias; "as alias" optional.
*/
if (preg_match_all("/^array<([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)*)>(?:\\s+as\\s+(.+))?$/", $input['class'], $collectionData)) {
$input['class'] = $collectionData[1][0];
$input['collection'] = true;
$input['collectionName'] = $collectionData[2][0];
} elseif (preg_match('/^array</', $input['class'])) { // See if a collection directive was attempted. Must be malformed.
throw new \InvalidArgumentException(
sprintf(
'Malformed collection directive: %s. Proper format is: array<Fully\\Qualified\\ClassName> or array<Fully\\Qualified\\ClassName> as collectionName',
$input['class']
)
);
}
// normalize groups
if (isset($input['groups']) && is_string($input['groups'])) {
$input['groups'] = array_map('trim', explode(',', $input['groups']));
}
return array_merge($defaults, $input);
}
/**
* Merges two parameter arrays together. This logic allows documentation to be built
* from multiple parser passes, with later passes extending previous passes:
* - Boolean values are true if any parser returns true.
* - Requirement parameters are concatenated.
* - Other string values are overridden by later parsers when present.
* - Array parameters are recursively merged.
* - Non-null default values prevail over null default values. Later values overrides previous defaults.
*
* However, if newly-returned parameter array contains a parameter with NULL, the parameter is removed from the merged results.
* If the parameter is not present in the newly-returned array, then it is left as-is.
*
* @param array $p1 The pre-existing parameters array.
* @param array $p2 The newly-returned parameters array.
*
* @return array The resulting, merged array.
*/
protected function mergeParameters($p1, $p2)
{
$params = $p1;
foreach ($p2 as $propname => $propvalue) {
if (null === $propvalue) {
unset($params[$propname]);
continue;
}
if (!isset($p1[$propname])) {
$params[$propname] = $propvalue;
} elseif (is_array($propvalue)) {
$v1 = $p1[$propname];
foreach ($propvalue as $name => $value) {
if (is_array($value)) {
if (isset($v1[$name]) && is_array($v1[$name])) {
$v1[$name] = $this->mergeParameters($v1[$name], $value);
} else {
$v1[$name] = $value;
}
} elseif (null !== $value) {
if (in_array($name, ['required', 'readonly'])) {
$v1[$name] = $v1[$name] || $value;
} elseif ('requirement' === $name) {
if (isset($v1[$name])) {
$v1[$name] .= ', ' . $value;
} else {
$v1[$name] = $value;
}
} elseif ('default' === $name) {
if (isset($v1[$name])) {
$v1[$name] = $value ?? $v1[$name];
} else {
$v1[$name] = $value ?? null;
}
} else {
$v1[$name] = $value;
}
}
}
$params[$propname] = $v1;
}
}
return $params;
}
/** /**
* Parses annotations for a given method, and adds new information to the given ApiDoc * Parses annotations for a given method, and adds new information to the given ApiDoc
* annotation. Useful to extract information from the FOSRestBundle annotations. * annotation. Useful to extract information from the FOSRestBundle annotations.
*
* @param ApiDoc $annotation
* @param Route $route
* @param ReflectionMethod $method
*/ */
protected function parseAnnotations(ApiDoc $annotation, Route $route, \ReflectionMethod $method): void protected function parseAnnotations(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
{ {
$annots = $this->reader->getMethodAnnotations($method);
foreach ($this->handlers as $handler) { foreach ($this->handlers as $handler) {
$handler->handle($annotation, $route, $method); $handler->handle($annotation, $annots, $route, $method);
} }
} }
/**
* Set parent class to children
*
* @param array $array The source array.
*
* @return array The updated array.
*/
protected function setParentClasses($array)
{
if (is_array($array)) {
foreach ($array as $k => $v) {
if (isset($v['children'])) {
if (isset($v['class'])) {
foreach ($v['children'] as $key => $item) {
if (empty($item['parentClass'] ?? null)) {
$array[$k]['children'][$key]['parentClass'] = $v['class'];
}
$array[$k]['children'][$key]['field'] = $key;
}
}
$array[$k]['children'] = $this->setParentClasses($array[$k]['children']);
}
}
}
return $array;
}
/**
* Clears the temporary 'class' parameter from the parameters array before it is returned.
*
* @param array $array The source array.
*
* @return array The cleared array.
*/
protected function clearClasses($array)
{
if (is_array($array)) {
unset($array['class']);
foreach ($array as $name => $item) {
$array[$name] = $this->clearClasses($item);
}
}
return $array;
}
/**
* Populates the `dataType` properties in the parameter array if empty. Recurses through children when necessary.
*
* @return array
*/
protected function generateHumanReadableTypes(array $array)
{
foreach ($array as $name => $info) {
if (empty($info['dataType']) && array_key_exists('subType', $info)) {
$array[$name]['dataType'] = $this->generateHumanReadableType($info['actualType'], $info['subType']);
}
if (isset($info['children'])) {
$array[$name]['children'] = $this->generateHumanReadableTypes($info['children']);
}
}
return $array;
}
/**
* Creates a human-readable version of the `actualType`. `subType` is taken into account.
*
* @param string $actualType
* @param string $subType
*
* @return string
*/
protected function generateHumanReadableType($actualType, $subType)
{
if (DataTypes::MODEL == $actualType) {
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('object (%s)', end($parts));
}
return sprintf('object (%s)', $subType);
}
if (DataTypes::COLLECTION == $actualType) {
if (DataTypes::isPrimitive($subType)) {
return sprintf('array of %ss', $subType);
}
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('array of objects (%s)', end($parts));
}
return sprintf('array of objects (%s)', $subType);
}
return $actualType;
}
private function getParsers(array $parameters)
{
if (isset($parameters['parsers'])) {
$parsers = [];
foreach ($this->parsers as $parser) {
if (in_array($parser::class, $parameters['parsers'])) {
$parsers[] = $parser;
}
}
} else {
$parsers = $this->parsers;
}
return $parsers;
}
} }

View file

@ -1,92 +0,0 @@
<?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\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\RouterInterface;
/**
* Class CachingApiDocExtractor
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CachingApiDocExtractor extends ApiDocExtractor
{
/**
* @param HandlerInterface[] $handlers
* @param string[] $excludeSections
* @param bool|false $debug
*/
public function __construct(
RouterInterface $router,
DocCommentExtractor $commentExtractor,
array $handlers,
array $excludeSections,
private string $cacheFile,
private bool $debug = false,
) {
parent::__construct($router, $commentExtractor, $handlers, $excludeSections);
}
/**
* @param string $view View name
*
* @return array|mixed
*/
public function all($view = ApiDoc::DEFAULT_VIEW): array
{
$cache = $this->getViewCache($view);
if (!$cache->isFresh()) {
$resources = [];
foreach ($this->getRoutes() as $route) {
if (
null !== ($method = $this->getReflectionMethod($route->getDefault('_controller')))
&& null !== $this->getMethodApiDoc($method)
) {
$file = $method->getDeclaringClass()->getFileName();
$resources[] = new FileResource($file);
}
}
$resources = array_merge($resources, $this->router->getRouteCollection()->getResources());
$data = parent::all($view);
$cache->write(serialize($data), $resources);
return $data;
}
// For BC
if (method_exists($cache, 'getPath')) {
$cachePath = $cache->getPath();
} else {
$cachePath = (string) $cache;
}
return unserialize(file_get_contents($cachePath));
}
/**
* @param string $view
*
* @return ConfigCache
*/
private function getViewCache($view)
{
return new ConfigCache($this->cacheFile . '.' . $view, $this->debug);
}
}

View file

@ -0,0 +1,48 @@
<?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\Extractor\Handler;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use FOS\RestBundle\Controller\Annotations\QueryParam;
class FosRestHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof RequestParam) {
$annotation->addParameter($annot->name, array(
'required' => $annot->strict && $annot->default === null,
'dataType' => $annot->requirements,
'description' => $annot->description,
'readonly' => false
));
} elseif ($annot instanceof QueryParam) {
if ($annot->strict && $annot->default === null) {
$annotation->addRequirement($annot->name, array(
'requirement' => $annot->requirements,
'dataType' => '',
'description' => $annot->description,
));
} else {
$annotation->addFilter($annot->name, array(
'requirement' => $annot->requirements,
'description' => $annot->description,
));
}
}
}
}
}

View file

@ -0,0 +1,30 @@
<?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\Extractor\Handler;
use JMS\SecurityExtraBundle\Annotation\PreAuthorize;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use JMS\SecurityExtraBundle\Annotation\Secure;
class JmsSecurityExtraHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof Secure || $annot instanceof PreAuthorize) {
$annotation->setAuthentication(true);
}
}
}
}

View file

@ -1,103 +0,0 @@
<?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\Extractor\Handler;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use Symfony\Component\Routing\Route;
class PhpDocHandler implements HandlerInterface
{
/**
* @var DocCommentExtractor
*/
protected $commentExtractor;
public function __construct(DocCommentExtractor $commentExtractor)
{
$this->commentExtractor = $commentExtractor;
}
public function handle(ApiDoc $annotation, Route $route, \ReflectionMethod $method): void
{
// description
if (null === $annotation->getDescription()) {
$comments = explode("\n", $annotation->getDocumentation());
// just set the first line
$comment = trim($comments[0]);
$comment = preg_replace("#\n+#", ' ', $comment);
$comment = preg_replace('#\s+#', ' ', $comment);
$comment = preg_replace('#[_`*]+#', '', $comment);
if ('@' !== substr($comment, 0, 1)) {
$annotation->setDescription($comment);
}
}
// requirements
$requirements = $annotation->getRequirements();
foreach ($route->getRequirements() as $name => $value) {
if (!isset($requirements[$name]) && '_method' !== $name && '_scheme' !== $name) {
$requirements[$name] = [
'requirement' => $value,
'dataType' => '',
'description' => '',
];
}
}
$paramDocs = [];
foreach (explode("\n", $this->commentExtractor->getDocComment($method)) as $line) {
if (preg_match('{^@param (.+)}', trim($line), $matches)) {
$paramDocs[] = $matches[1];
}
if (preg_match('{^@deprecated}', trim($line))) {
$annotation->setDeprecated(true);
}
if (preg_match('{^@(link|see) (.+)}', trim($line), $matches)) {
$annotation->setLink($matches[2]);
}
}
$regexp = '{(\w*) *\$%s\b *(.*)}i';
foreach ($route->compile()->getVariables() as $var) {
$found = false;
foreach ($paramDocs as $paramDoc) {
if (preg_match(sprintf($regexp, preg_quote($var)), $paramDoc, $matches)) {
$annotationRequirements = $annotation->getRequirements();
if (!isset($annotationRequirements[$var]['dataType'])) {
$requirements[$var]['dataType'] = $matches[1] ?? '';
}
if (!isset($annotationRequirements[$var]['description'])) {
$requirements[$var]['description'] = $matches[2];
}
if (!isset($requirements[$var]['requirement']) && !isset($annotationRequirements[$var]['requirement'])) {
$requirements[$var]['requirement'] = '';
}
$found = true;
break;
}
}
if (!isset($requirements[$var]) && false === $found) {
$requirements[$var] = ['requirement' => '', 'dataType' => '', 'description' => ''];
}
}
$annotation->setRequirements($requirements);
}
}

View file

@ -0,0 +1,29 @@
<?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\Extractor\Handler;
use Nelmio\ApiDocBundle\Extractor\HandlerInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class SensioFrameworkExtraHandler implements HandlerInterface
{
public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method)
{
foreach ($annotations as $annot) {
if ($annot instanceof Cache) {
$annotation->setCache($annot->getMaxAge());
}
}
}
}

View file

@ -11,13 +11,18 @@
namespace Nelmio\ApiDocBundle\Extractor; namespace Nelmio\ApiDocBundle\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Route;
interface HandlerInterface interface HandlerInterface
{ {
/** /**
* Parse route parameters in order to populate ApiDoc. * Parse route parameters in order to populate ApiDoc.
*
* @param Nelmio\ApiDocBundle\Annotation\ApiDoc $annotation
* @param array $annotations
* @param Symfony\Component\Routing\Route $route
* @param ReflectionMethod $method
*/ */
public function handle(ApiDoc $annotation, Route $route, \ReflectionMethod $method); public function handle(ApiDoc $annotation, array $annotations, Route $route, \ReflectionMethod $method);
} }

View file

@ -11,43 +11,45 @@
namespace Nelmio\ApiDocBundle\Form\Extension; namespace Nelmio\ApiDocBundle\Form\Extension;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DescriptionFormTypeExtension extends AbstractTypeExtension class DescriptionFormTypeExtension extends AbstractTypeExtension
{ {
public function buildForm(FormBuilderInterface $builder, array $options): void /**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder->setAttribute('description', $options['description']); $builder->setAttribute('description', $options['description']);
} }
public function buildView(FormView $view, FormInterface $form, array $options): void /**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{ {
$view->vars['description'] = $options['description']; $view->vars['description'] = $options['description'];
} }
/** /**
* @deprecated Remove it when bumping requirements to Symfony 2.7+ * {@inheritdoc}
*/ */
public function setDefaultOptions(OptionsResolverInterface $resolver): void public function setDefaultOptions(OptionsResolverInterface $resolver)
{ {
$this->configureOptions($resolver); $resolver->setDefaults(array(
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'description' => '', 'description' => '',
]); ));
} }
public static function getExtendedTypes(): iterable /**
* {@inheritdoc}
*/
public function getExtendedType()
{ {
return [LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\FormType')]; return 'form';
} }
} }

View file

@ -11,18 +11,13 @@
namespace Nelmio\ApiDocBundle\Formatter; namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
abstract class AbstractFormatter implements FormatterInterface abstract class AbstractFormatter implements FormatterInterface
{ {
protected $version; /**
* {@inheritdoc}
public function setVersion($version): void */
{
$this->version = $version;
}
public function formatOne(ApiDoc $annotation) public function formatOne(ApiDoc $annotation)
{ {
return $this->renderOne( return $this->renderOne(
@ -30,6 +25,9 @@ abstract class AbstractFormatter implements FormatterInterface
); );
} }
/**
* {@inheritdoc}
*/
public function format(array $collection) public function format(array $collection)
{ {
return $this->render( return $this->render(
@ -40,6 +38,7 @@ abstract class AbstractFormatter implements FormatterInterface
/** /**
* Format a single array of data * Format a single array of data
* *
* @param array $data
* @return string|array * @return string|array
*/ */
abstract protected function renderOne(array $data); abstract protected function renderOne(array $data);
@ -47,70 +46,34 @@ abstract class AbstractFormatter implements FormatterInterface
/** /**
* Format a set of resource sections. * Format a set of resource sections.
* *
* @param array $collection
* @return string|array * @return string|array
*/ */
abstract protected function render(array $collection); abstract protected function render(array $collection);
/**
* Check that the versions range includes current version
*
* @param string $fromVersion (default: null)
* @param string $toVersion (default: null)
*
* @return bool
*/
protected function rangeIncludesVersion($fromVersion = null, $toVersion = null)
{
if (!$fromVersion && !$toVersion) {
return true;
}
if ($fromVersion && version_compare($fromVersion, $this->version, '>')) {
return false;
}
if ($toVersion && version_compare($toVersion, $this->version, '<')) {
return false;
}
return true;
}
/** /**
* Compresses nested parameters into a flat by changing the parameter * Compresses nested parameters into a flat by changing the parameter
* names to strings which contain the nested property names, for example: * names to strings which contain the nested property names, for example:
* `user[group][name]` * `user[group][name]`
* *
* @param string $parentName
* @param bool $ignoreNestedReadOnly
* *
* @param array $data
* @param string $parentName
* @param boolean $ignoreNestedReadOnly
* @return array * @return array
*/ */
protected function compressNestedParameters(array $data, $parentName = null, $ignoreNestedReadOnly = false) protected function compressNestedParameters(array $data, $parentName = null, $ignoreNestedReadOnly = false)
{ {
$newParams = []; $newParams = array();
foreach ($data as $name => $info) { foreach ($data as $name => $info) {
if ($this->version && !$this->rangeIncludesVersion(
$info['sinceVersion'] ?? null,
$info['untilVersion'] ?? null
)) {
continue;
}
$newName = $this->getNewName($name, $info, $parentName); $newName = $this->getNewName($name, $info, $parentName);
$newParams[$newName] = [ $newParams[$newName] = array(
'dataType' => $info['dataType'], 'description' => $info['description'],
'readonly' => array_key_exists('readonly', $info) ? $info['readonly'] : null, 'dataType' => $info['dataType'],
'required' => $info['required'], 'readonly' => $info['readonly'],
'default' => array_key_exists('default', $info) ? $info['default'] : null, 'required' => $info['required'],
'description' => array_key_exists('description', $info) ? $info['description'] : null, );
'format' => array_key_exists('format', $info) ? $info['format'] : null,
'sinceVersion' => array_key_exists('sinceVersion', $info) ? $info['sinceVersion'] : null,
'untilVersion' => array_key_exists('untilVersion', $info) ? $info['untilVersion'] : null,
'actualType' => array_key_exists('actualType', $info) ? $info['actualType'] : null,
'subType' => array_key_exists('subType', $info) ? $info['subType'] : null,
'parentClass' => array_key_exists('parentClass', $info) ? $info['parentClass'] : null,
'field' => array_key_exists('field', $info) ? $info['field'] : null,
];
if (isset($info['children']) && (!$info['readonly'] || !$ignoreNestedReadOnly)) { if (isset($info['children']) && (!$info['readonly'] || !$ignoreNestedReadOnly)) {
foreach ($this->compressNestedParameters($info['children'], $newName, $ignoreNestedReadOnly) as $nestedItemName => $nestedItemData) { foreach ($this->compressNestedParameters($info['children'], $newName, $ignoreNestedReadOnly) as $nestedItemName => $nestedItemData) {
@ -126,29 +89,21 @@ abstract class AbstractFormatter implements FormatterInterface
* Returns a new property name, taking into account whether or not the property * Returns a new property name, taking into account whether or not the property
* is an array of some other data type. * is an array of some other data type.
* *
* @param string $name * @param string $name
* @param array $data * @param array $data
* @param string $parentName * @param string $parentName
*
* @return string * @return string
*/ */
protected function getNewName($name, $data, $parentName = null) protected function getNewName($name, $data, $parentName = null)
{ {
$array = ''; $newName = ($parentName) ? sprintf("%s[%s]", $parentName, $name) : $name;
$newName = ($parentName) ? sprintf('%s[%s]', $parentName, $name) : $name; $array = (false === strpos($data['dataType'], "array of")) ? "" : "[]";
if (isset($data['actualType']) && DataTypes::COLLECTION == $data['actualType'] return sprintf("%s%s", $newName, $array);
&& isset($data['subType']) && null !== $data['subType']
) {
$array = '[]';
}
return sprintf('%s%s', $newName, $array);
} }
/** /**
* @param array $annotation * @param array $annotation
*
* @return array * @return array
*/ */
protected function processAnnotation($annotation) protected function processAnnotation($annotation)
@ -161,30 +116,21 @@ abstract class AbstractFormatter implements FormatterInterface
$annotation['response'] = $this->compressNestedParameters($annotation['response']); $annotation['response'] = $this->compressNestedParameters($annotation['response']);
} }
if (isset($annotation['parsedResponseMap'])) {
foreach ($annotation['parsedResponseMap'] as $statusCode => &$data) {
$data['model'] = $this->compressNestedParameters($data['model']);
}
}
$annotation['id'] = strtolower($annotation['method'] ?? '') . '-' . str_replace('/', '-', $annotation['uri'] ?? '');
return $annotation; return $annotation;
} }
/** /**
* @param array[ApiDoc] $collection * @param array[ApiDoc] $collection
*
* @return array * @return array
*/ */
protected function processCollection(array $collection) protected function processCollection(array $collection)
{ {
$array = []; $array = array();
foreach ($collection as $coll) { foreach ($collection as $coll) {
$array[$coll['annotation']->getSection()][$coll['resource']][] = $coll['annotation']->toArray(); $array[$coll['annotation']->getSection()][$coll['resource']][] = $coll['annotation']->toArray();
} }
$processedCollection = []; $processedCollection = array();
foreach ($array as $section => $resources) { foreach ($array as $section => $resources) {
foreach ($resources as $path => $annotations) { foreach ($resources as $path => $annotations) {
foreach ($annotations as $annotation) { foreach ($annotations as $annotation) {

View file

@ -11,7 +11,7 @@
namespace Nelmio\ApiDocBundle\Formatter; namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
interface FormatterInterface interface FormatterInterface
{ {
@ -19,7 +19,6 @@ interface FormatterInterface
* Format a collection of documentation data. * Format a collection of documentation data.
* *
* @param array[ApiDoc] $collection * @param array[ApiDoc] $collection
*
* @return string|array * @return string|array
*/ */
public function format(array $collection); public function format(array $collection);
@ -28,7 +27,7 @@ interface FormatterInterface
* Format documentation data for one route. * Format documentation data for one route.
* *
* @param ApiDoc $annotation * @param ApiDoc $annotation
* return string|array * return string|array
*/ */
public function formatOne(ApiDoc $annotation); public function formatOne(ApiDoc $annotation);
} }

View file

@ -12,7 +12,6 @@
namespace Nelmio\ApiDocBundle\Formatter; namespace Nelmio\ApiDocBundle\Formatter;
use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\EngineInterface;
use Twig\Environment as TwigEnvironment;
class HtmlFormatter extends AbstractFormatter class HtmlFormatter extends AbstractFormatter
{ {
@ -32,20 +31,15 @@ class HtmlFormatter extends AbstractFormatter
protected $defaultRequestFormat; protected $defaultRequestFormat;
/** /**
* @var EngineInterface|TwigEnvironment * @var EngineInterface
*/ */
protected $engine; protected $engine;
/** /**
* @var bool * @var boolean
*/ */
private $enableSandbox; private $enableSandbox;
/**
* @var array
*/
private $requestFormats;
/** /**
* @var string * @var string
*/ */
@ -56,16 +50,6 @@ class HtmlFormatter extends AbstractFormatter
*/ */
private $acceptType; private $acceptType;
/**
* @var array
*/
private $bodyFormats;
/**
* @var string
*/
private $defaultBodyFormat;
/** /**
* @var array * @var array
*/ */
@ -77,11 +61,9 @@ class HtmlFormatter extends AbstractFormatter
private $motdTemplate; private $motdTemplate;
/** /**
* @var bool * @param array $authentication
*/ */
private $defaultSectionsOpened; public function setAuthentication(array $authentication = null)
public function setAuthentication(?array $authentication = null): void
{ {
$this->authentication = $authentication; $this->authentication = $authentication;
} }
@ -89,7 +71,7 @@ class HtmlFormatter extends AbstractFormatter
/** /**
* @param string $apiName * @param string $apiName
*/ */
public function setApiName($apiName): void public function setApiName($apiName)
{ {
$this->apiName = $apiName; $this->apiName = $apiName;
} }
@ -97,23 +79,23 @@ class HtmlFormatter extends AbstractFormatter
/** /**
* @param string $endpoint * @param string $endpoint
*/ */
public function setEndpoint($endpoint): void public function setEndpoint($endpoint)
{ {
$this->endpoint = $endpoint; $this->endpoint = $endpoint;
} }
/** /**
* @param bool $enableSandbox * @param boolean $enableSandbox
*/ */
public function setEnableSandbox($enableSandbox): void public function setEnableSandbox($enableSandbox)
{ {
$this->enableSandbox = $enableSandbox; $this->enableSandbox = $enableSandbox;
} }
/** /**
* @param EngineInterface|TwigEnvironment $engine * @param EngineInterface $engine
*/ */
public function setTemplatingEngine($engine): void public function setTemplatingEngine(EngineInterface $engine)
{ {
$this->engine = $engine; $this->engine = $engine;
} }
@ -121,41 +103,23 @@ class HtmlFormatter extends AbstractFormatter
/** /**
* @param string $acceptType * @param string $acceptType
*/ */
public function setAcceptType($acceptType): void public function setAcceptType($acceptType)
{ {
$this->acceptType = $acceptType; $this->acceptType = $acceptType;
} }
public function setBodyFormats(array $bodyFormats): void
{
$this->bodyFormats = $bodyFormats;
}
/**
* @param string $defaultBodyFormat
*/
public function setDefaultBodyFormat($defaultBodyFormat): void
{
$this->defaultBodyFormat = $defaultBodyFormat;
}
/** /**
* @param string $method * @param string $method
*/ */
public function setRequestFormatMethod($method): void public function setRequestFormatMethod($method)
{ {
$this->requestFormatMethod = $method; $this->requestFormatMethod = $method;
} }
public function setRequestFormats(array $formats): void
{
$this->requestFormats = $formats;
}
/** /**
* @param string $format * @param string $format
*/ */
public function setDefaultRequestFormat($format): void public function setDefaultRequestFormat($format)
{ {
$this->defaultRequestFormat = $format; $this->defaultRequestFormat = $format;
} }
@ -163,7 +127,7 @@ class HtmlFormatter extends AbstractFormatter
/** /**
* @param string $motdTemplate * @param string $motdTemplate
*/ */
public function setMotdTemplate($motdTemplate): void public function setMotdTemplate($motdTemplate)
{ {
$this->motdTemplate = $motdTemplate; $this->motdTemplate = $motdTemplate;
} }
@ -177,30 +141,28 @@ class HtmlFormatter extends AbstractFormatter
} }
/** /**
* @param bool $defaultSectionsOpened * {@inheritdoc}
*/ */
public function setDefaultSectionsOpened($defaultSectionsOpened): void
{
$this->defaultSectionsOpened = $defaultSectionsOpened;
}
protected function renderOne(array $data) protected function renderOne(array $data)
{ {
return $this->engine->render('@NelmioApiDoc/resource.html.twig', array_merge( return $this->engine->render('NelmioApiDocBundle::resource.html.twig', array_merge(
[ array(
'data' => $data, 'data' => $data,
'displayContent' => true, 'displayContent' => true,
], ),
$this->getGlobalVars() $this->getGlobalVars()
)); ));
} }
/**
* {@inheritdoc}
*/
protected function render(array $collection) protected function render(array $collection)
{ {
return $this->engine->render('@NelmioApiDoc/resources.html.twig', array_merge( return $this->engine->render('NelmioApiDocBundle::resources.html.twig', array_merge(
[ array(
'resources' => $collection, 'resources' => $collection,
], ),
$this->getGlobalVars() $this->getGlobalVars()
)); ));
} }
@ -210,22 +172,18 @@ class HtmlFormatter extends AbstractFormatter
*/ */
private function getGlobalVars() private function getGlobalVars()
{ {
return [ return array(
'apiName' => $this->apiName, 'apiName' => $this->apiName,
'authentication' => $this->authentication, 'authentication' => $this->authentication,
'endpoint' => $this->endpoint, 'endpoint' => $this->endpoint,
'enableSandbox' => $this->enableSandbox, 'enableSandbox' => $this->enableSandbox,
'requestFormatMethod' => $this->requestFormatMethod, 'requestFormatMethod' => $this->requestFormatMethod,
'acceptType' => $this->acceptType, 'acceptType' => $this->acceptType,
'bodyFormats' => $this->bodyFormats,
'defaultBodyFormat' => $this->defaultBodyFormat,
'requestFormats' => $this->requestFormats,
'defaultRequestFormat' => $this->defaultRequestFormat, 'defaultRequestFormat' => $this->defaultRequestFormat,
'date' => date(DATE_RFC822), 'date' => date(DATE_RFC822),
'css' => file_get_contents(__DIR__ . '/../Resources/public/css/screen.css'), 'css' => file_get_contents(__DIR__ . '/../Resources/public/css/screen.css'),
'js' => file_get_contents(__DIR__ . '/../Resources/public/js/all.js'), 'js' => file_get_contents(__DIR__ . '/../Resources/public/js/all.js'),
'motdTemplate' => $this->motdTemplate, 'motdTemplate' => $this->motdTemplate
'defaultSectionsOpened' => $this->defaultSectionsOpened, );
];
} }
} }

View file

@ -13,12 +13,15 @@ namespace Nelmio\ApiDocBundle\Formatter;
class MarkdownFormatter extends AbstractFormatter class MarkdownFormatter extends AbstractFormatter
{ {
/**
* {@inheritdoc}
*/
protected function renderOne(array $data) protected function renderOne(array $data)
{ {
$markdown = sprintf("### `%s` %s ###\n", $data['method'], $data['uri']); $markdown = sprintf("### `%s` %s ###\n", $data['method'], $data['uri']);
if (isset($data['deprecated']) && false !== $data['deprecated']) { if(isset($data['deprecated'])) {
$markdown .= '### This method is deprecated ###'; $markdown .= "### This method is deprecated ###";
$markdown .= "\n\n"; $markdown .= "\n\n";
} }
@ -83,9 +86,6 @@ class MarkdownFormatter extends AbstractFormatter
if (isset($parameter['description']) && !empty($parameter['description'])) { if (isset($parameter['description']) && !empty($parameter['description'])) {
$markdown .= sprintf(" * description: %s\n", $parameter['description']); $markdown .= sprintf(" * description: %s\n", $parameter['description']);
} }
if (isset($parameter['default']) && !empty($parameter['default'])) {
$markdown .= sprintf(" * default value: %s\n", $parameter['default']);
}
$markdown .= "\n"; $markdown .= "\n";
} }
@ -103,20 +103,6 @@ class MarkdownFormatter extends AbstractFormatter
$markdown .= sprintf(" * description: %s\n", $parameter['description']); $markdown .= sprintf(" * description: %s\n", $parameter['description']);
} }
if (null !== $parameter['sinceVersion'] || null !== $parameter['untilVersion']) {
$markdown .= ' * versions: ';
if ($parameter['sinceVersion']) {
$markdown .= '>=' . $parameter['sinceVersion'];
}
if ($parameter['untilVersion']) {
if ($parameter['sinceVersion']) {
$markdown .= ',';
}
$markdown .= '<=' . $parameter['untilVersion'];
}
$markdown .= "\n";
}
$markdown .= "\n"; $markdown .= "\n";
} }
} }
@ -124,6 +110,9 @@ class MarkdownFormatter extends AbstractFormatter
return $markdown; return $markdown;
} }
/**
* {@inheritdoc}
*/
protected function render(array $collection) protected function render(array $collection)
{ {
$markdown = ''; $markdown = '';

View file

@ -1,70 +0,0 @@
<?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;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
/**
* Extends SwaggerFormatter which takes into account the request's base URL when generating the documents for direct swagger-ui consumption.
*
* @author Bezalel Hermoso <bezalelhermoso@gmail.com>
*/
class RequestAwareSwaggerFormatter implements FormatterInterface
{
/**
* @var Request
*/
protected $request;
/**
* @var SwaggerFormatter
*/
protected $formatter;
public function __construct(Request $request, SwaggerFormatter $formatter)
{
$this->request = $request;
$this->formatter = $formatter;
}
/**
* Format a collection of documentation data.
*
* @param null $resource
*
* @internal param $array [ApiDoc] $collection
*
* @return string|array
*/
public function format(array $collection, $resource = null)
{
$result = $this->formatter->format($collection, $resource);
if (null !== $resource) {
$result['basePath'] = $this->request->getBaseUrl() . $result['basePath'];
}
return $result;
}
/**
* Format documentation data for one route.
*
* @param ApiDoc $annotation
* return string|array
*/
public function formatOne(ApiDoc $annotation)
{
return $this->formatter->formatOne($annotation);
}
}

View file

@ -11,33 +11,42 @@
namespace Nelmio\ApiDocBundle\Formatter; namespace Nelmio\ApiDocBundle\Formatter;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
class SimpleFormatter extends AbstractFormatter class SimpleFormatter extends AbstractFormatter
{ {
/**
* {@inheritdoc}
*/
public function formatOne(ApiDoc $annotation) public function formatOne(ApiDoc $annotation)
{ {
return $annotation->toArray(); return $annotation->toArray();
} }
/**
* {@inheritdoc}
*/
public function format(array $collection) public function format(array $collection)
{ {
$array = []; $array = array();
foreach ($collection as $coll) { foreach ($collection as $coll) {
$annotationArray = $coll['annotation']->toArray(); $array[$coll['resource']][] = $coll['annotation']->toArray();
unset($annotationArray['parsedResponseMap']);
$array[$coll['resource']][] = $annotationArray;
} }
return $array; return $array;
} }
protected function renderOne(array $data): void /**
* {@inheritdoc}
*/
protected function renderOne(array $data)
{ {
} }
protected function render(array $collection): void /**
* {@inheritdoc}
*/
protected function render(array $collection)
{ {
} }
} }

View file

@ -1,565 +0,0 @@
<?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;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\DataTypes;
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;
protected $info = [];
protected $typeMap = [
DataTypes::INTEGER => 'integer',
DataTypes::FLOAT => 'number',
DataTypes::STRING => 'string',
DataTypes::BOOLEAN => 'boolean',
DataTypes::FILE => 'string',
DataTypes::DATE => 'string',
DataTypes::DATETIME => 'string',
];
protected $formatMap = [
DataTypes::INTEGER => 'int32',
DataTypes::FLOAT => 'float',
DataTypes::FILE => 'byte',
DataTypes::DATE => 'date',
DataTypes::DATETIME => 'date-time',
];
/**
* @var ModelRegistry
*/
protected $modelRegistry;
public function __construct($namingStategy)
{
$this->modelRegistry = new ModelRegistry($namingStategy);
}
/**
* @var array
*/
protected $authConfig;
public function setAuthenticationConfig(array $config): void
{
$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.
*
* @param array|ApiDoc[] $collection
* @param string|null $resource
*
* @return string|array
*/
public function format(array $collection, $resource = null)
{
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)
{
$resourceList = [
'swaggerVersion' => (string) $this->swaggerVersion,
'apis' => [],
'apiVersion' => (string) $this->apiVersion,
'info' => $this->getInfo(),
'authorizations' => $this->getAuthorizations(),
];
$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);
$apis[] = [
'path' => '/' . $normalizedName,
'description' => $apiDoc->getResourceDescription(),
];
}
return $resourceList;
}
protected function getAuthorizations()
{
$auth = [];
if (null === $this->authConfig) {
return $auth;
}
$config = $this->authConfig;
if ('http' === $config['delivery']) {
return $auth;
}
$auth['apiKey'] = [
'type' => 'apiKey',
'passAs' => $config['delivery'],
'keyname' => $config['name'],
];
return $auth;
}
/**
* @return array
*/
protected function getInfo()
{
return $this->info;
}
/**
* Format documentation data for one route.
*
* @param ApiDoc $annotation
* return string|array
*
* @throws \BadMethodCallException
*/
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.
*
* @param string $resource
*
* @return array
*/
protected function produceApiDeclaration(array $collection, $resource)
{
$apiDeclaration = [
'swaggerVersion' => (string) $this->swaggerVersion,
'apiVersion' => (string) $this->apiVersion,
'basePath' => $this->basePath,
'resourcePath' => $resource,
'apis' => [],
'models' => [],
'produces' => [],
'consumes' => [],
'authorizations' => $this->getAuthorizations(),
];
$main = null;
$apiBag = [];
foreach ($collection as $item) {
/** @var $apiDoc ApiDoc */
$apiDoc = $item['annotation'];
$itemResource = $this->stripBasePath($item['resource']);
$input = $apiDoc->getInput();
if (!is_array($input)) {
$input = [
'class' => $input,
'paramType' => 'form',
];
} 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])) {
$apiBag[$path] = [];
}
$parameters = [];
$responseMessages = [];
foreach ($compiled->getPathVariables() as $paramValue) {
$parameter = [
'paramType' => 'path',
'name' => $paramValue,
'type' => 'string',
'required' => true,
];
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'])) {
$parameters = array_merge($parameters, $this->deriveParameters($data['parameters'], $input['paramType']));
}
$responseMap = $apiDoc->getParsedResponseMap();
$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'];
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,
[
$alias => [
'dataType' => null,
'subType' => $className,
'actualType' => DataTypes::COLLECTION,
'required' => true,
'readonly' => true,
'description' => null,
'default' => null,
'children' => $prop['model'][$alias]['children'],
],
],
''
);
$responseModel = [
'code' => $statusCode,
'message' => $message,
'responseModel' => $collId,
];
} else {
$responseModel = [
'code' => $statusCode,
'message' => $message,
'responseModel' => $this->registerModel($className, $prop['model'], ''),
];
}
$responseMessages[$statusCode] = $responseModel;
}
$unmappedMessages = array_diff(array_keys($statusMessages), array_keys($responseMessages));
foreach ($unmappedMessages as $code) {
$responseMessages[$code] = [
'code' => $code,
'message' => is_array($statusMessages[$code]) ? implode('; ', $statusMessages[$code]) : $statusMessages[$code],
];
}
$type = $responseMessages[200]['responseModel'] ?? null;
foreach ($apiDoc->getRoute()->getMethods() as $method) {
$operation = [
'method' => $method,
'summary' => $apiDoc->getDescription(),
'nickname' => $this->generateNickname($method, $itemResource),
'parameters' => $parameters,
'responseMessages' => array_values($responseMessages),
];
if (null !== $type) {
$operation['type'] = $type;
}
$apiBag[$path][] = $operation;
}
}
$apiDeclaration['resourcePath'] = $resource;
foreach ($apiBag as $path => $operations) {
$apiDeclaration['apis'][] = [
'path' => $path,
'operations' => $operations,
];
}
$apiDeclaration['models'] = $this->modelRegistry->getModels();
$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);
return $path;
}
public function setBasePath($path): void
{
$this->basePath = $path;
}
/**
* Formats query parameters to Swagger-compliant form.
*
* @return array
*/
protected function deriveQueryParameters(array $input)
{
$parameters = [];
foreach ($input as $name => $prop) {
if (!isset($prop['dataType'])) {
$prop['dataType'] = 'string';
}
$parameters[] = [
'paramType' => 'query',
'name' => $name,
'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
*/
protected function deriveParameters(array $input, $paramType = 'form')
{
$parameters = [];
foreach ($input as $name => $prop) {
$type = null;
$format = null;
$ref = null;
$enum = null;
$items = null;
if (!isset($prop['actualType'])) {
$prop['actualType'] = 'string';
}
if (isset($this->typeMap[$prop['actualType']])) {
$type = $this->typeMap[$prop['actualType']];
} else {
switch ($prop['actualType']) {
case DataTypes::ENUM:
$type = 'string';
if (isset($prop['format'])) {
$enum = explode('|', rtrim(ltrim($prop['format'], '['), ']'));
}
break;
case DataTypes::MODEL:
$ref =
$this->registerModel(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
break;
case DataTypes::COLLECTION:
$type = 'array';
if (null === $prop['subType']) {
$items = ['type' => 'string'];
} elseif (isset($this->typeMap[$prop['subType']])) {
$items = ['type' => $this->typeMap[$prop['subType']]];
} else {
$ref =
$this->registerModel(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
$items = [
'$ref' => $ref,
];
}
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;
}
$parameter = [
'paramType' => $paramType,
'name' => $name,
];
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'])) {
$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
*
* @internal param $models
*/
public function registerModel($className, ?array $parameters = null, $description = '')
{
return $this->modelRegistry->register($className, $parameters, $description);
}
public function setSwaggerVersion($swaggerVersion): void
{
$this->swaggerVersion = $swaggerVersion;
}
public function setApiVersion($apiVersion): void
{
$this->apiVersion = $apiVersion;
}
public function setInfo($info): void
{
$this->info = $info;
}
/**
* Strips the base path from a URL path.
*/
protected function stripBasePath($basePath)
{
if ('/' === $this->basePath) {
return $basePath;
}
$path = sprintf('#^%s#', preg_quote($this->basePath));
$subPath = preg_replace($path, '', $basePath);
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);
return sprintf('%s_%s', strtolower($method ?: ''), $resource);
}
}

View file

@ -1,19 +0,0 @@
ifneq (,$(shell (type docker-compose 2>&1 >/dev/null && echo 1) || true))
PHP=docker-compose run --rm --no-deps php
else
PHP=php
endif
PHP_CONSOLE_DEPS=vendor
vendor: composer.json
@$(PHP) composer install -o -n --no-ansi
@touch vendor || true
php-cs: $(PHP_CONSOLE_DEPS)
@$(PHP) vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --using-cache=no -v
phpunit: $(PHP_CONSOLE_DEPS)
@$(PHP) vendor/bin/phpunit --color=always
check: phpunit

View file

@ -2,24 +2,20 @@
namespace Nelmio\ApiDocBundle; namespace Nelmio\ApiDocBundle;
use Nelmio\ApiDocBundle\DependencyInjection\ExtractorHandlerCompilerPass;
use Nelmio\ApiDocBundle\DependencyInjection\FormInfoParserCompilerPass;
use Nelmio\ApiDocBundle\DependencyInjection\LoadExtractorParsersPass;
use Nelmio\ApiDocBundle\DependencyInjection\RegisterExtractorParsersPass;
use Nelmio\ApiDocBundle\DependencyInjection\SwaggerConfigCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Nelmio\ApiDocBundle\DependencyInjection\RegisterJmsParserPass;
use Nelmio\ApiDocBundle\DependencyInjection\RegisterExtractorParsersPass;
use Nelmio\ApiDocBundle\DependencyInjection\ExtractorHandlerCompilerPass;
class NelmioApiDocBundle extends Bundle class NelmioApiDocBundle extends Bundle
{ {
public function build(ContainerBuilder $container): void public function build(ContainerBuilder $container)
{ {
parent::build($container); parent::build($container);
$container->addCompilerPass(new LoadExtractorParsersPass()); $container->addCompilerPass(new RegisterJmsParserPass());
$container->addCompilerPass(new RegisterExtractorParsersPass()); $container->addCompilerPass(new RegisterExtractorParsersPass());
$container->addCompilerPass(new ExtractorHandlerCompilerPass()); $container->addCompilerPass(new ExtractorHandlerCompilerPass());
$container->addCompilerPass(new SwaggerConfigCompilerPass());
$container->addCompilerPass(new FormInfoParserCompilerPass());
} }
} }

View file

@ -1,74 +0,0 @@
<?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\Parser;
use Nelmio\ApiDocBundle\DataTypes;
/**
* Handles models that are specified as collections.
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CollectionParser implements ParserInterface, PostParserInterface
{
/**
* Return true/false whether this class supports parsing the given class.
*
* @param array $item containing the following fields: class, groups. Of which groups is optional
*
* @return bool
*/
public function supports(array $item)
{
return isset($item['collection']) && true === $item['collection'];
}
/**
* This doesn't parse anything at this stage.
*
* @return array
*/
public function parse(array $item)
{
return [];
}
/**
* @param array|string $item The string type of input to parse.
* @param array $parameters The previously-parsed parameters array.
*
* @return array
*/
public function postParse(array $item, array $parameters)
{
$origParameters = $parameters;
foreach ($parameters as $name => $body) {
$parameters[$name] = null;
}
$collectionName = $item['collectionName'] ?? '';
$parameters[$collectionName] = [
'dataType' => null, // Delegates to ApiDocExtractor#generateHumanReadableTypes
'subType' => $item['class'],
'actualType' => DataTypes::COLLECTION,
'readonly' => true,
'required' => true,
'default' => true,
'description' => '',
'children' => $origParameters,
];
return $parameters;
}
}

View file

@ -1,124 +0,0 @@
<?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\Parser;
use Nelmio\ApiDocBundle\DataTypes;
/**
* @author Bez Hermoso <bezalelhermoso@gmail.com>
*/
class FormErrorsParser implements ParserInterface, PostParserInterface
{
/**
* Return true/false whether this class supports parsing the given class.
*
* @param array $item containing the following fields: class, groups. Of which groups is optional
*
* @return bool
*/
public function supports(array $item)
{
return isset($item['form_errors']) && true === $item['form_errors'];
}
public function parse(array $item)
{
return [];
}
/**
* Overrides the root parameters to contain these parameters instead:
* - status_code: 400
* - message: "Validation failed"
* - errors: contains the original parameters, but all types are changed to array of strings (array of errors for each field)
*
* @return array
*/
public function postParse(array $item, array $parameters)
{
$params = $parameters;
foreach ($params as $name => $data) {
$params[$name] = null;
}
$params['status_code'] = [
'dataType' => 'integer',
'actualType' => DataTypes::INTEGER,
'subType' => null,
'required' => false,
'description' => 'The status code',
'readonly' => true,
'default' => 400,
];
$params['message'] = [
'dataType' => 'string',
'actualType' => DataTypes::STRING,
'subType' => null,
'required' => false,
'description' => 'The error message',
'default' => 'Validation failed.',
];
$params['errors'] = [
'dataType' => 'errors',
'actualType' => DataTypes::MODEL,
'subType' => sprintf('%s.FormErrors', $item['class']),
'required' => false,
'description' => 'Errors',
'readonly' => true,
'children' => $this->doPostParse($parameters),
];
return $params;
}
protected function doPostParse(array $parameters, $attachFieldErrors = true, array $propertyPath = [])
{
$data = [];
foreach ($parameters as $name => $parameter) {
$data[$name] = [
'dataType' => 'parameter errors',
'actualType' => DataTypes::MODEL,
'subType' => 'FieldErrors',
'required' => false,
'description' => 'Errors on the parameter',
'readonly' => true,
'children' => [
'errors' => [
'dataType' => 'array of errors',
'actualType' => DataTypes::COLLECTION,
'subType' => 'string',
'required' => false,
'dscription' => '',
'readonly' => true,
],
],
];
if (DataTypes::MODEL === $parameter['actualType']) {
$propertyPath[] = $name;
$data[$name]['subType'] = sprintf('%s.FieldErrors[%s]', $parameter['subType'], implode('.', $propertyPath));
$data[$name]['children'] = $this->doPostParse($parameter['children'], $attachFieldErrors, $propertyPath);
} else {
if (false === $attachFieldErrors) {
unset($data[$name]['children']);
}
$attachFieldErrors = false;
}
}
return $data;
}
}

View file

@ -1,11 +0,0 @@
<?php
namespace Nelmio\ApiDocBundle\Parser;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormTypeInterface;
interface FormInfoParser
{
public function parseFormType(FormTypeInterface $type, FormConfigInterface $config): ?array;
}

View file

@ -11,126 +11,46 @@
namespace Nelmio\ApiDocBundle\Parser; namespace Nelmio\ApiDocBundle\Parser;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\Form\ResolvedFormTypeInterface;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Component\Form\FormRegistry;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\Exception\FormException;
class FormTypeParser implements ParserInterface class FormTypeParser implements ParserInterface
{ {
/** /**
* @var FormFactoryInterface * @var \Symfony\Component\Form\FormFactoryInterface
*/ */
protected $formFactory; protected $formFactory;
/**
* @var \Symfony\Component\Form\FormRegistry
*/
protected $formRegistry;
/**
* @var \Symfony\Component\Translation\TranslatorInterface
*/
protected $translator;
/**
* @var bool
*/
protected $entityToChoice;
/**
* @var array|FormInfoParser[]
*/
protected $formInfoParsers = [];
/**
* @var array
*
* @deprecated since 2.12, to be removed in 3.0. Use $extendedMapTypes instead.
*/
protected $mapTypes = [
'text' => DataTypes::STRING,
'date' => DataTypes::DATE,
'datetime' => DataTypes::DATETIME,
'checkbox' => DataTypes::BOOLEAN,
'time' => DataTypes::TIME,
'number' => DataTypes::FLOAT,
'integer' => DataTypes::INTEGER,
'textarea' => DataTypes::STRING,
'country' => DataTypes::STRING,
'choice' => DataTypes::ENUM,
'file' => DataTypes::FILE,
];
/** /**
* @var array * @var array
*/ */
protected $extendedMapTypes = [ protected $mapTypes = array(
DataTypes::STRING => [ 'text' => 'string',
'text', 'date' => 'date',
'Symfony\Component\Form\Extension\Core\Type\TextType', 'datetime' => 'datetime',
'textarea', 'checkbox' => 'boolean',
'Symfony\Component\Form\Extension\Core\Type\TextareaType', 'time' => 'time',
'country', 'number' => 'float',
'Symfony\Component\Form\Extension\Core\Type\CountryType', 'integer' => 'int',
], 'textarea' => 'string',
DataTypes::DATE => [ 'country' => 'string',
'date', );
'Symfony\Component\Form\Extension\Core\Type\DateType',
],
DataTypes::DATETIME => [
'datetime',
'Symfony\Component\Form\Extension\Core\Type\DatetimeType',
],
DataTypes::BOOLEAN => [
'checkbox',
'Symfony\Component\Form\Extension\Core\Type\CheckboxType',
],
DataTypes::TIME => [
'time',
'Symfony\Component\Form\Extension\Core\Type\TimeType',
],
DataTypes::FLOAT => [
'number',
'Symfony\Component\Form\Extension\Core\Type\NumberType',
],
DataTypes::INTEGER => [
'integer',
'Symfony\Component\Form\Extension\Core\Type\IntegerType',
],
DataTypes::ENUM => [
'choice',
'Symfony\Component\Form\Extension\Core\Type\ChoiceType',
],
DataTypes::FILE => [
'file',
'Symfony\Component\Form\Extension\Core\Type\FileType',
],
];
public function __construct(FormFactoryInterface $formFactory, TranslatorInterface $translator, $entityToChoice) public function __construct(FormFactoryInterface $formFactory, FormRegistry $formRegistry)
{ {
$this->formFactory = $formFactory; $this->formFactory = $formFactory;
$this->translator = $translator; $this->formRegistry = $formRegistry;
$this->entityToChoice = (bool) $entityToChoice;
} }
public function supports(array $item) /**
* {@inheritdoc}
*/
public function supports($item)
{ {
$className = $item['class'];
$options = $item['options'];
try { try {
if ($this->createForm($className, null, $options)) { if ($this->createForm($item)) {
return true; return true;
} }
} catch (FormException $e) { } catch (FormException $e) {
@ -142,159 +62,36 @@ class FormTypeParser implements ParserInterface
return false; return false;
} }
public function parse(array $item) /**
* {@inheritdoc}
*/
public function parse($type)
{ {
$type = $item['class']; if ($this->implementsType($type)) {
$options = $item['options']; $type = $this->getTypeInstance($type);
try {
$form = $this->formFactory->create($type, null, $options);
}
// TODO: find a better exception to catch
catch (\Exception $exception) {
if (!LegacyFormHelper::isLegacy()) {
@trigger_error('Using FormTypeInterface instance with required arguments without defining them as service is deprecated in symfony 2.8 and removed in 3.0.', E_USER_DEPRECATED);
}
} }
if (!isset($form)) { $form = $this->formFactory->create($type);
if (!LegacyFormHelper::hasBCBreaks() && $this->implementsType($type)) {
$type = $this->getTypeInstance($type);
$form = $this->formFactory->create($type, null, $options);
} else {
throw new \InvalidArgumentException('Unsupported form type class.');
}
}
$name = array_key_exists('name', $item) return $this->parseForm($form, $form->getName());
? $item['name']
: (method_exists($form, 'getBlockPrefix') ? $form->getBlockPrefix() : $form->getName());
if (empty($name)) {
return $this->parseForm($form);
}
$subType = is_object($type) ? $type::class : $type;
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $subType);
}
return [
$name => [
'required' => true,
'readonly' => false,
'description' => '',
'default' => null,
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'subType' => $subType,
'children' => $this->parseForm($form),
],
];
} }
public function addFormInfoParser(FormInfoParser $formInfoParser): void private function parseForm($form, $prefix = null)
{ {
$class = $formInfoParser::class; $parameters = array();
if (isset($this->formInfoParsers[$class])) {
throw new \InvalidArgumentException($class . ' already added');
}
$this->formInfoParsers[$class] = $formInfoParser;
}
private function parseFormType(FormTypeInterface $type, FormConfigInterface $config): ?array
{
foreach ($this->formInfoParsers as $parser) {
$customInfo = $parser->parseFormType($type, $config);
if ($customInfo) {
return $customInfo;
}
}
return null;
}
private function getDataType($type)
{
foreach ($this->extendedMapTypes as $data => $types) {
if (in_array($type, $types)) {
return $data;
}
}
}
private function parseForm($form)
{
$parameters = [];
$domain = $form->getConfig()->getOption('translation_domain');
foreach ($form as $name => $child) { foreach ($form as $name => $child) {
$config = $child->getConfig(); $config = $child->getConfig();
$options = $config->getOptions();
if ($prefix) {
$name = sprintf('%s[%s]', $prefix, $name);
}
$bestType = ''; $bestType = '';
$actualType = null; for ($type = $config->getType(); null !== $type; $type = $type->getParent()) {
$subType = null; if (isset($this->mapTypes[$type->getName()])) {
$children = null; $bestType = $this->mapTypes[$type->getName()];
} elseif ('collection' === $type->getName() && isset($this->mapTypes[$config->getOption('type')])) {
for ($type = $config->getType(); $bestType = sprintf('array of %ss', $this->mapTypes[$config->getOption('type')]);
$type instanceof FormInterface || $type instanceof ResolvedFormTypeInterface;
$type = $type->getParent()
) {
$customInfo = $this->parseFormType($type->getInnerType(), $config);
if ($customInfo) {
$parameters[$name] = array_merge([
'dataType' => 'string',
'actualType' => 'string',
'default' => $config->getData(),
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
], $customInfo);
continue 2;
}
$typeName = method_exists($type, 'getBlockPrefix') ?
$type->getBlockPrefix() : $type->getName();
$dataType = $this->getDataType($typeName);
if (null !== $dataType) {
$actualType = $bestType = $dataType;
} elseif ('collection' === $typeName) {
// BC sf < 2.8
$typeOption = $config->hasOption('entry_type') ? $config->getOption('entry_type') : $config->getOption('type');
if (is_object($typeOption)) {
$typeOption = method_exists($typeOption, 'getBlockPrefix') ?
$typeOption->getBlockPrefix() : $typeOption->getName();
}
$dataType = $this->getDataType($typeOption);
if (null !== $dataType) {
$subType = $dataType;
$actualType = DataTypes::COLLECTION;
$bestType = sprintf('array of %ss', $subType);
} else {
// Embedded form collection
// BC sf < 2.8
$embbededType = $config->hasOption('entry_type') ? $config->getOption('entry_type') : $config->getOption('type');
$subForm = $this->formFactory->create($embbededType, null, $config->getOption('entry_options', []));
$children = $this->parseForm($subForm);
$actualType = DataTypes::COLLECTION;
$subType = is_object($embbededType) ? $embbededType::class : $embbededType;
if ($subType && class_exists($subType)) {
$parts = explode('\\', $subType);
$bestType = sprintf('array of objects (%s)', end($parts));
} else {
$bestType = sprintf('array of objects (%s)', $subType);
}
}
} }
} }
@ -308,39 +105,10 @@ class FormTypeParser implements ParserInterface
*/ */
$addDefault = false; $addDefault = false;
try { try {
if (isset($subForm)) { $subForm = $this->formFactory->create($type);
unset($subForm);
}
if (LegacyFormHelper::hasBCBreaks()) {
try {
$subForm = $this->formFactory->create($type::class, null, $options);
} catch (\Exception $e) {
}
}
if (!isset($subForm)) {
$subForm = $this->formFactory->create($type, null, $options);
}
$subParameters = $this->parseForm($subForm, $name); $subParameters = $this->parseForm($subForm, $name);
if (!empty($subParameters)) { if (!empty($subParameters)) {
$children = $subParameters; $parameters = array_merge($parameters, $subParameters);
$config = $subForm->getConfig();
$subType = $type::class;
$parts = explode('\\', $subType);
$bestType = sprintf('object (%s)', end($parts));
$parameters[$name] = [
'dataType' => $bestType,
'actualType' => DataTypes::MODEL,
'default' => null,
'subType' => $subType,
'required' => $config->getRequired(),
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
'children' => $children,
];
} else { } else {
$addDefault = true; $addDefault = true;
} }
@ -349,14 +117,12 @@ class FormTypeParser implements ParserInterface
} }
if ($addDefault) { if ($addDefault) {
$parameters[$name] = [ $parameters[$name] = array(
'dataType' => 'string', 'dataType' => 'string',
'actualType' => 'string', 'required' => $config->getRequired(),
'default' => $config->getData(), 'description' => $config->getAttribute('description'),
'required' => $config->getRequired(), 'readonly' => $config->getDisabled(),
'description' => $this->getFormDescription($config, $domain), );
'readonly' => $config->getDisabled(),
];
} }
continue; continue;
@ -364,67 +130,12 @@ class FormTypeParser implements ParserInterface
} }
} }
$parameters[$name] = [ $parameters[$name] = array(
'dataType' => $bestType, 'dataType' => $bestType,
'actualType' => $actualType, 'required' => $config->getRequired(),
'subType' => $subType, 'description' => $config->getAttribute('description'),
'default' => $config->getData(), 'readonly' => $config->getDisabled(),
'required' => $config->getRequired(), );
'description' => $this->getFormDescription($config, $domain),
'readonly' => $config->getDisabled(),
];
if (null !== $children) {
$parameters[$name]['children'] = $children;
}
switch ($bestType) {
case 'datetime':
if (($format = $config->getOption('date_format')) && is_string($format)) {
$parameters[$name]['format'] = $format;
} elseif ('single_text' == $config->getOption('widget') && $format = $config->getOption('format')) {
$parameters[$name]['format'] = $format;
}
break;
case 'date':
if (($format = $config->getOption('format')) && is_string($format)) {
$parameters[$name]['format'] = $format;
}
break;
case 'choice':
if ($config->getOption('multiple')) {
$parameters[$name]['dataType'] = sprintf('array of %ss', $parameters[$name]['dataType']);
$parameters[$name]['actualType'] = DataTypes::COLLECTION;
$parameters[$name]['subType'] = DataTypes::ENUM;
}
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
$choices = $config->getOption('choices_as_values') ?
array_values($choices) :
array_keys($choices);
sort($choices);
$parameters[$name]['format'] = '[' . implode('|', $choices) . ']';
} elseif ($choiceList = $config->getOption('choice_list')) {
$choiceListType = $config->getType();
$choiceListName = method_exists($choiceListType, 'getBlockPrefix') ?
$choiceListType->getBlockPrefix() : $choiceListType->getName();
if ('entity' === $choiceListName && false === $this->entityToChoice) {
$choices = [];
} else {
// TODO: fixme
// does not work since: https://github.com/symfony/symfony/commit/03efce1b568379eac21d880e427090e43035f505
$choices = [];
}
if (is_array($choices) && count($choices)) {
$parameters[$name]['format'] = json_encode($choices);
}
}
break;
}
} }
return $parameters; return $parameters;
@ -432,75 +143,28 @@ class FormTypeParser implements ParserInterface
private function implementsType($item) private function implementsType($item)
{ {
if (null === $item || !class_exists($item)) { if (!class_exists($item)) {
return false; return false;
} }
$refl = new \ReflectionClass($item); $refl = new \ReflectionClass($item);
return $refl->implementsInterface('Symfony\Component\Form\FormTypeInterface') || $refl->implementsInterface('Symfony\Component\Form\ResolvedFormTypeInterface'); return $refl->implementsInterface('Symfony\Component\Form\FormTypeInterface');
} }
private function getTypeInstance($type) private function getTypeInstance($type)
{ {
$refl = new \ReflectionClass($type); return unserialize(sprintf('O:%d:"%s":0:{}', strlen($type), $type));
$constructor = $refl->getConstructor();
// this fallback may lead to runtime exception, but try hard to generate the docs
if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
return $refl->newInstanceWithoutConstructor();
}
return $refl->newInstance();
} }
private function createForm($type, $data = null, array $options = []) private function createForm($item)
{ {
try { if ($this->implementsType($item)) {
return $this->formFactory->create($type, null, $options); $type = $this->getTypeInstance($item);
} catch (InvalidArgumentException $exception) {
return $this->formFactory->create($type);
} }
if ($this->formRegistry->hasType($item)) {
if (!LegacyFormHelper::hasBCBreaks() && !isset($form) && $this->implementsType($type)) { return $this->formFactory->create($item);
$type = $this->getTypeInstance($type);
return $this->formFactory->create($type, null, $options);
} }
} }
private function handleChoiceListValues(ChoiceListInterface $choiceList)
{
$choices = [];
foreach ([$choiceList->getPreferredViews(), $choiceList->getRemainingViews()] as $viewList) {
$choices = array_merge($choices, $this->handleChoiceViewsHierarchy($viewList));
}
return $choices;
}
private function handleChoiceViewsHierarchy(array $choiceViews)
{
$choices = [];
foreach ($choiceViews as $item) {
if ($item instanceof ChoiceView) {
$choices[$item->value] = $item->label;
} elseif (is_array($item)) {
$choices = array_merge($choices, $this->handleChoiceViewsHierarchy($item));
}
}
return $choices;
}
private function getFormDescription($config, $domain = null)
{
$description = ($config->getOption('description'))
?: $config->getOption('label');
if (null != $description) {
return $this->translator->trans($description, [], $domain);
}
return null;
}
} }

View file

@ -11,22 +11,20 @@
namespace Nelmio\ApiDocBundle\Parser; namespace Nelmio\ApiDocBundle\Parser;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy; use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata; use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\SerializationContext;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
/** /**
* Uses the JMS metadata factory to extract input/output model information * Uses the JMS metadata factory to extract input/output model information
*/ */
class JmsMetadataParser implements ParserInterface, PostParserInterface class JmsMetadataParser implements ParserInterface
{ {
/** /**
* @var MetadataFactoryInterface * @var \Metadata\MetadataFactoryInterface
*/ */
private $factory; private $factory;
@ -36,39 +34,30 @@ class JmsMetadataParser implements ParserInterface, PostParserInterface
private $namingStrategy; private $namingStrategy;
/** /**
* @var DocCommentExtractor * @var \Nelmio\ApiDocBundle\Util\DocCommentExtractor
*/ */
private $commentExtractor; private $commentExtractor;
private $typeMap = [
'integer' => DataTypes::INTEGER,
'boolean' => DataTypes::BOOLEAN,
'string' => DataTypes::STRING,
'float' => DataTypes::FLOAT,
'double' => DataTypes::FLOAT,
'array' => DataTypes::COLLECTION,
'DateTime' => DataTypes::DATETIME,
];
/** /**
* Constructor, requires JMS Metadata factory * Constructor, requires JMS Metadata factory
*/ */
public function __construct( public function __construct(
MetadataFactoryInterface $factory, MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy, PropertyNamingStrategyInterface $namingStrategy,
DocCommentExtractor $commentExtractor, DocCommentExtractor $commentExtractor
) { ) {
$this->factory = $factory; $this->factory = $factory;
$this->namingStrategy = $namingStrategy; $this->namingStrategy = $namingStrategy;
$this->commentExtractor = $commentExtractor; $this->commentExtractor = $commentExtractor;
} }
public function supports(array $input) /**
* {@inheritdoc}
*/
public function supports($input)
{ {
$className = $input['class'];
try { try {
if ($meta = $this->factory->getMetadataForClass($className)) { if ($meta = $this->factory->getMetadataForClass($input)) {
return true; return true;
} }
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
@ -77,109 +66,45 @@ class JmsMetadataParser implements ParserInterface, PostParserInterface
return false; return false;
} }
public function parse(array $input) /**
* {@inheritdoc}
*/
public function parse($input)
{ {
$className = $input['class']; return $this->doParse($input);
$groups = $input['groups'];
$result = $this->doParse($className, [], $groups);
if (!isset($input['name']) || empty($input['name'])) {
return $result;
}
if ($className && class_exists($className)) {
$parts = explode('\\', $className);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $className);
}
return [
$input['name'] => [
'required' => null,
'readonly' => null,
'default' => null,
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'subType' => $dataType,
'children' => $result,
],
];
} }
/** /**
* Recursively parse all metadata for a class * Recursively parse all metadata for a class
* *
* @param string $className Class to get all metadata for * @param string $className Class to get all metadata for
* @param array $visited Classes we've already visited to prevent infinite recursion. * @param array $visited Classes we've already visited to prevent infinite recursion.
* @param array $groups Serialization groups to include. * @return array metadata for given class
*
* @return array metadata for given class
*
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
protected function doParse($className, $visited = [], array $groups = []) protected function doParse($className, $visited = array())
{ {
$meta = $this->factory->getMetadataForClass($className); $meta = $this->factory->getMetadataForClass($className);
if (null === $meta) { if (null === $meta) {
throw new \InvalidArgumentException(sprintf('No metadata found for class %s', $className)); throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
} }
$exclusionStrategies = []; $params = array();
if ($groups) {
$exclusionStrategies[] = new GroupsExclusionStrategy($groups);
}
$params = [];
$reflection = new \ReflectionClass($className);
$defaultProperties = array_map(function ($default) {
if (is_array($default) && 0 === count($default)) {
return null;
}
return $default;
}, $reflection->getDefaultProperties());
// iterate over property metadata // iterate over property metadata
foreach ($meta->propertyMetadata as $item) { foreach ($meta->propertyMetadata as $item) {
if (null !== $item->type) { if (!is_null($item->type)) {
$name = $this->namingStrategy->translateName($item); $name = $this->namingStrategy->translateName($item);
$dataType = $this->processDataType($item); $dataType = $this->processDataType($item);
// apply exclusion strategies $params[$name] = array(
foreach ($exclusionStrategies as $strategy) { 'dataType' => $dataType['normalized'],
if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) { 'required' => false, //TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
continue 2; 'description' => $this->getDescription($className, $item),
} 'readonly' => $item->readOnly
} );
if (!$dataType['inline']) {
$params[$name] = [
'dataType' => $dataType['normalized'],
'actualType' => $dataType['actualType'],
'subType' => $dataType['class'],
'required' => false,
'default' => $defaultProperties[$item->name] ?? null,
// TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
'description' => $this->getDescription($item),
'readonly' => $item->readOnly,
'sinceVersion' => $item->sinceVersion,
'untilVersion' => $item->untilVersion,
];
if (null !== $dataType['class'] && false === $dataType['primitive']) {
$params[$name]['class'] = $dataType['class'];
}
}
// we can use type property also for custom handlers, then we don't have here real class name
if (!$dataType['class'] || !class_exists($dataType['class'])) {
continue;
}
// if class already parsed, continue, to avoid infinite recursion // if class already parsed, continue, to avoid infinite recursion
if (in_array($dataType['class'], $visited)) { if (in_array($dataType['class'], $visited)) {
@ -187,15 +112,9 @@ class JmsMetadataParser implements ParserInterface, PostParserInterface
} }
// check for nested classes with JMS metadata // check for nested classes with JMS metadata
if ($dataType['class'] && false === $dataType['primitive'] && null !== $this->factory->getMetadataForClass($dataType['class'])) { if ($dataType['class'] && null !== $this->factory->getMetadataForClass($dataType['class'])) {
$visited[] = $dataType['class']; $visited[] = $dataType['class'];
$children = $this->doParse($dataType['class'], $visited, $groups); $params[$name]['children'] = $this->doParse($dataType['class'], $visited);
if ($dataType['inline']) {
$params = array_merge($params, $children);
} else {
$params[$name]['children'] = $children;
}
} }
} }
} }
@ -207,6 +126,7 @@ class JmsMetadataParser implements ParserInterface, PostParserInterface
* Figure out a normalized data type (for documentation), and get a * Figure out a normalized data type (for documentation), and get a
* nested class name, if available. * nested class name, if available.
* *
* @param PropertyMetadata $type
* @return array * @return array
*/ */
protected function processDataType(PropertyMetadata $item) protected function processDataType(PropertyMetadata $item)
@ -214,127 +134,74 @@ class JmsMetadataParser implements ParserInterface, PostParserInterface
// check for a type inside something that could be treated as an array // check for a type inside something that could be treated as an array
if ($nestedType = $this->getNestedTypeInArray($item)) { if ($nestedType = $this->getNestedTypeInArray($item)) {
if ($this->isPrimitive($nestedType)) { if ($this->isPrimitive($nestedType)) {
return [ return array(
'normalized' => sprintf('array of %ss', $nestedType), 'normalized' => sprintf("array of %ss", $nestedType),
'actualType' => DataTypes::COLLECTION, 'class' => null
'class' => $this->typeMap[$nestedType], );
'primitive' => true,
'inline' => false,
];
} }
$exp = explode('\\', $nestedType); $exp = explode("\\", $nestedType);
return [ return array(
'normalized' => sprintf('array of objects (%s)', end($exp)), 'normalized' => sprintf("array of objects (%s)", end($exp)),
'actualType' => DataTypes::COLLECTION, 'class' => $nestedType
'class' => $nestedType, );
'primitive' => false,
'inline' => false,
];
} }
$type = $item->type['name']; $type = $item->type['name'];
// could be basic type // could be basic type
if ($this->isPrimitive($type)) { if ($this->isPrimitive($type)) {
return [ return array(
'normalized' => $type, 'normalized' => $type,
'actualType' => $this->typeMap[$type], 'class' => null
'class' => null, );
'primitive' => true,
'inline' => false,
];
}
// we can use type property also for custom handlers, then we don't have here real class name
if (!$type || !class_exists($type)) {
return [
'normalized' => sprintf('custom handler result for (%s)', $type),
'class' => $type,
'actualType' => DataTypes::MODEL,
'primitive' => false,
'inline' => false,
];
} }
// if we got this far, it's a general class name // if we got this far, it's a general class name
$exp = explode('\\', $type); $exp = explode("\\", $type);
return [ return array(
'normalized' => sprintf('object (%s)', end($exp)), 'normalized' => sprintf("object (%s)", end($exp)),
'class' => $type, 'class' => $type
'actualType' => DataTypes::MODEL, );
'primitive' => false,
'inline' => $item->inline,
];
} }
protected function isPrimitive($type) protected function isPrimitive($type)
{ {
return in_array($type, ['boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime']); return in_array($type, array('boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime'));
}
public function postParse(array $input, array $parameters)
{
return $this->doPostParse($parameters, [], $input['groups'] ?? []);
}
/**
* Recursive `doPostParse` to avoid circular post parsing.
*
* @return array
*/
protected function doPostParse(array $parameters, array $visited = [], array $groups = [])
{
foreach ($parameters as $param => $data) {
if (isset($data['class']) && isset($data['children']) && !in_array($data['class'], $visited)) {
$visited[] = $data['class'];
$input = ['class' => $data['class'], 'groups' => $data['groups'] ?? []];
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->doPostParse($parameters[$param]['children'], $visited, $groups)
);
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->doParse($input['class'], $visited, $groups)
);
}
}
return $parameters;
} }
/** /**
* Check the various ways JMS describes values in arrays, and * Check the various ways JMS describes values in arrays, and
* get the value type in the array * get the value type in the array
* *
* @param PropertyMetadata $item
* @return string|null * @return string|null
*/ */
protected function getNestedTypeInArray(PropertyMetadata $item) protected function getNestedTypeInArray(PropertyMetadata $item)
{ {
if (isset($item->type['name']) && in_array($item->type['name'], ['array', 'ArrayCollection'])) { if (is_array($item->type)
if (isset($item->type['params'][1]['name'])) { && in_array($item->type['name'], array('array', 'ArrayCollection'))
// E.g. array<string, MyNamespaceMyObject> && isset($item->type['params'])
return $item->type['params'][1]['name']; && 1 === count($item->type['params'])
} && isset($item->type['params'][0]['name'])) {
if (isset($item->type['params'][0]['name'])) { return $item->type['params'][0]['name'];
// E.g. array<MyNamespaceMyObject>
return $item->type['params'][0]['name'];
}
} }
return null; return null;
} }
protected function getDescription(PropertyMetadata $item) protected function getDescription($className, PropertyMetadata $item)
{ {
$ref = new \ReflectionClass($item->class); $ref = new \ReflectionClass($className);
if ($item instanceof VirtualPropertyMetadata) { if ($item instanceof VirtualPropertyMetadata) {
$extracted = $this->commentExtractor->getDocCommentText($ref->getMethod($item->getter)); $extracted = $this->commentExtractor->getDocCommentText($ref->getMethod($item->getter));
} else { } else {
$extracted = $this->commentExtractor->getDocCommentText($ref->getProperty($item->name)); $extracted = $this->commentExtractor->getDocCommentText($ref->getProperty($item->name));
} }
return $extracted; return !empty($extracted) ? $extracted : "No description.";
} }
} }

View file

@ -1,90 +0,0 @@
<?php
/**
* Created by mcfedr on 30/06/15 21:03
*/
namespace Nelmio\ApiDocBundle\Parser;
class JsonSerializableParser implements ParserInterface
{
public function supports(array $item)
{
if (!is_subclass_of($item['class'], 'JsonSerializable')) {
return false;
}
$ref = new \ReflectionClass($item['class']);
if ($ref->hasMethod('__construct')) {
foreach ($ref->getMethod('__construct')->getParameters() as $parameter) {
if (!$parameter->isOptional()) {
return false;
}
}
}
return true;
}
public function parse(array $input)
{
/** @var \JsonSerializable $obj */
$obj = new $input['class']();
$encoded = $obj->jsonSerialize();
$parsed = $this->getItemMetaData($encoded);
if (isset($input['name']) && !empty($input['name'])) {
$output = [];
$output[$input['name']] = $parsed;
return $output;
}
return $parsed['children'];
}
public function getItemMetaData($item)
{
$type = gettype($item);
$meta = [
'dataType' => 'NULL' == $type ? null : $type,
'actualType' => $type,
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
'default' => is_scalar($item) ? $item : null,
];
if ('object' == $type && $item instanceof \JsonSerializable) {
$meta = $this->getItemMetaData($item->jsonSerialize());
$meta['class'] = $item::class;
} elseif (('object' == $type && $item instanceof \stdClass) || ('array' == $type && !$this->isSequential($item))) {
$meta['dataType'] = 'object';
$meta['children'] = [];
foreach ($item as $key => $value) {
$meta['children'][$key] = $this->getItemMetaData($value);
}
}
return $meta;
}
/**
* Check for numeric sequential keys, just like the json encoder does
* Credit: http://stackoverflow.com/a/25206156/859027
*
* @return bool
*/
private function isSequential(array $arr)
{
for ($i = count($arr) - 1; $i >= 0; --$i) {
if (!isset($arr[$i]) && !array_key_exists($i, $arr)) {
return false;
}
}
return true;
}
}

View file

@ -19,11 +19,10 @@ interface ParserInterface
/** /**
* Return true/false whether this class supports parsing the given class. * Return true/false whether this class supports parsing the given class.
* *
* @param array $item containing the following fields: class, groups. Of which groups is optional * @param string $item The string type of input to parse.
* * @return boolean
* @return bool
*/ */
public function supports(array $item); public function supports($item);
/** /**
* Returns an array of class property metadata where each item is a key (the property name) and * Returns an array of class property metadata where each item is a key (the property name) and
@ -34,12 +33,10 @@ interface ParserInterface
* - readonly boolean * - readonly boolean
* - children (optional) array of nested property names mapped to arrays * - children (optional) array of nested property names mapped to arrays
* in the format described here * in the format described here
* - class (optional) the fully-qualified class name of the item, if
* it is represented by an object
*
* @param array $item The string type of input to parse.
* *
* @param string $item The string type of input to parse.
* @return array * @return array
*/ */
public function parse(array $item); public function parse($item);
} }

View file

@ -1,39 +0,0 @@
<?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\Parser;
/**
* This is the interface parsers must implement in order to register a second parsing pass after the initial structure
* is populated..
*/
interface PostParserInterface
{
/**
* Reparses an object for additional documentation details after it has already been parsed once, to allow
* parsers to extend information initially documented by other parsers.
*
* Returns an array of class property metadata where each item is a key (the property name) and
* an array of data with the following keys:
* - dataType string
* - required boolean
* - description string
* - readonly boolean
* - children (optional) array of nested property names mapped to arrays
* in the format described here
*
* @param string $item The string type of input to parse.
* @param array $parameters The previously-parsed parameters array.
*
* @return array
*/
public function postParse(array $item, array $parameters);
}

View file

@ -1,353 +0,0 @@
<?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\Parser;
use Nelmio\ApiDocBundle\DataTypes;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Symfony\Component\Validator\MetadataFactoryInterface as LegacyMetadataFactoryInterface;
/**
* Uses the Symfony Validation component to extract information about API objects.
*/
class ValidationParser implements ParserInterface, PostParserInterface
{
/**
* @var LegacyMetadataFactoryInterface
*/
protected $factory;
protected $typeMap = [
'integer' => DataTypes::INTEGER,
'int' => DataTypes::INTEGER,
'scalar' => DataTypes::STRING,
'numeric' => DataTypes::INTEGER,
'boolean' => DataTypes::BOOLEAN,
'string' => DataTypes::STRING,
'float' => DataTypes::FLOAT,
'double' => DataTypes::FLOAT,
'long' => DataTypes::INTEGER,
'object' => DataTypes::MODEL,
'array' => DataTypes::COLLECTION,
'DateTime' => DataTypes::DATETIME,
];
/**
* Requires a validation MetadataFactory.
*
* @param MetadataFactoryInterface|LegacyMetadataFactoryInterface $factory
*/
public function __construct($factory)
{
if (!($factory instanceof MetadataFactoryInterface) && !($factory instanceof LegacyMetadataFactoryInterface)) {
throw new \InvalidArgumentException('Argument 1 of %s constructor must be either an instance of Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface or Symfony\Component\Validator\MetadataFactoryInterface.');
}
$this->factory = $factory;
}
public function supports(array $input)
{
$className = $input['class'];
return $this->factory->hasMetadataFor($className);
}
public function parse(array $input)
{
$className = $input['class'];
$groups = [];
if (isset($input['groups']) && $input['groups']) {
$groups = $input['groups'];
}
$parsed = $this->doParse($className, [], $groups);
if (!isset($input['name']) || empty($input['name'])) {
return $parsed;
}
if ($className && class_exists($className)) {
$parts = explode('\\', $className);
$dataType = sprintf('object (%s)', end($parts));
} else {
$dataType = sprintf('object (%s)', $className);
}
return [
$input['name'] => [
'dataType' => $dataType,
'actualType' => DataTypes::MODEL,
'class' => $className,
'subType' => $dataType,
'required' => null,
'readonly' => null,
'children' => $parsed,
'default' => null,
],
];
}
/**
* Recursively parse constraints.
*
* @return array
*/
protected function doParse($className, array $visited, array $groups = [])
{
$params = [];
$classdata = $this->factory->getMetadataFor($className);
$properties = $classdata->getConstrainedProperties();
$refl = $classdata->getReflectionClass();
$defaults = $refl->getDefaultProperties();
foreach ($properties as $property) {
$vparams = [];
$vparams['default'] = $defaults[$property] ?? null;
$pds = $classdata->getPropertyMetadata($property);
foreach ($pds as $propdata) {
$constraints = $propdata->getConstraints();
foreach ($constraints as $constraint) {
$vparams = $this->parseConstraint($constraint, $vparams, $className, $visited, $groups);
}
}
if (isset($vparams['format'])) {
$vparams['format'] = implode(', ', array_unique($vparams['format']));
}
foreach (['dataType', 'readonly', 'required', 'subType'] as $reqprop) {
if (!isset($vparams[$reqprop])) {
$vparams[$reqprop] = null;
}
}
// check for nested classes with All constraint
if (isset($vparams['class']) && !in_array($vparams['class'], $visited) && null !== $this->factory->getMetadataFor($vparams['class'])) {
$visited[] = $vparams['class'];
$vparams['children'] = $this->doParse($vparams['class'], $visited, $groups);
}
$vparams['actualType'] = $vparams['actualType'] ?? DataTypes::STRING;
$params[$property] = $vparams;
}
return $params;
}
public function postParse(array $input, array $parameters)
{
$groups = [];
if (isset($input['groups']) && $input['groups']) {
$groups = $input['groups'];
}
foreach ($parameters as $param => $data) {
if (isset($data['class']) && isset($data['children'])) {
$input = ['class' => $data['class'], 'groups' => $groups];
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->postParse($input, $parameters[$param]['children'])
);
$parameters[$param]['children'] = array_merge(
$parameters[$param]['children'], $this->parse($input, $parameters[$param]['children'])
);
}
}
return $parameters;
}
/**
* Create a valid documentation parameter based on an individual validation Constraint.
* Currently supports:
* - NotBlank/NotNull
* - Type
* - Email
* - Url
* - Ip
* - Length (min and max)
* - Choice (single and multiple, min and max)
* - Regex (match and non-match)
*
* @param Constraint $constraint The constraint metadata object.
* @param array $vparams The existing validation parameters.
* @param array $groups Validation groups.
*
* @return mixed The parsed list of validation parameters.
*/
protected function parseConstraint(
Constraint $constraint,
$vparams,
$className,
&$visited = [],
array $groups = [],
) {
$class = substr($constraint::class, strlen('Symfony\\Component\\Validator\\Constraints\\'));
$vparams['actualType'] = DataTypes::STRING;
$vparams['subType'] = null;
$vparams['groups'] = $constraint->groups;
if ($groups) {
$containGroup = false;
foreach ($groups as $group) {
if (in_array($group, $vparams['groups'])) {
$containGroup = true;
}
}
if (!$containGroup) {
return $vparams;
}
}
switch ($class) {
case 'NotBlank':
$vparams['format'][] = '{not blank}';
// no break
case 'NotNull':
$vparams['required'] = true;
break;
case 'Type':
if (isset($this->typeMap[$constraint->type])) {
$vparams['actualType'] = $this->typeMap[$constraint->type];
}
$vparams['dataType'] = $constraint->type;
break;
case 'Email':
$vparams['format'][] = '{email address}';
break;
case 'Url':
$vparams['format'][] = '{url}';
break;
case 'Ip':
$vparams['format'][] = '{ip address}';
break;
case 'Date':
$vparams['format'][] = '{Date YYYY-MM-DD}';
$vparams['actualType'] = DataTypes::DATE;
break;
case 'DateTime':
$vparams['format'][] = '{DateTime YYYY-MM-DD HH:MM:SS}';
$vparams['actualType'] = DataTypes::DATETIME;
break;
case 'Time':
$vparams['format'][] = '{Time HH:MM:SS}';
$vparams['actualType'] = DataTypes::TIME;
break;
case 'Range':
$messages = [];
if (isset($constraint->min)) {
$messages[] = ">={$constraint->min}";
}
if (isset($constraint->max)) {
$messages[] = "<={$constraint->max}";
}
$vparams['format'][] = '{range: {' . implode(', ', $messages) . '}}';
break;
case 'Length':
$messages = [];
if (isset($constraint->min)) {
$messages[] = "min: {$constraint->min}";
}
if (isset($constraint->max)) {
$messages[] = "max: {$constraint->max}";
}
$vparams['format'][] = '{length: {' . implode(', ', $messages) . '}}';
break;
case 'Choice':
$choices = $this->getChoices($constraint, $className);
sort($choices);
$format = '[' . implode('|', $choices) . ']';
if ($constraint->multiple) {
$vparams['actualType'] = DataTypes::COLLECTION;
$vparams['subType'] = DataTypes::ENUM;
$messages = [];
if (isset($constraint->min)) {
$messages[] = "min: {$constraint->min} ";
}
if (isset($constraint->max)) {
$messages[] = "max: {$constraint->max} ";
}
$vparams['format'][] = '{' . implode('', $messages) . 'choice of ' . $format . '}';
} else {
$vparams['actualType'] = DataTypes::ENUM;
$vparams['format'][] = $format;
}
break;
case 'Regex':
if ($constraint->match) {
$vparams['format'][] = '{match: ' . $constraint->pattern . '}';
} else {
$vparams['format'][] = '{not match: ' . $constraint->pattern . '}';
}
break;
case 'All':
foreach ($constraint->constraints as $childConstraint) {
if ($childConstraint instanceof Type) {
$nestedType = $childConstraint->type;
$exp = explode('\\', $nestedType);
if (!$nestedType || !class_exists($nestedType)) {
$nestedType = substr($className, 0, strrpos($className, '\\') + 1) . $nestedType;
if (!class_exists($nestedType)) {
continue;
}
}
$vparams['dataType'] = sprintf('array of objects (%s)', end($exp));
$vparams['actualType'] = DataTypes::COLLECTION;
$vparams['subType'] = $nestedType;
$vparams['class'] = $nestedType;
if (!in_array($nestedType, $visited)) {
$visited[] = $nestedType;
$vparams['children'] = $this->doParse($nestedType, $visited);
}
}
}
break;
}
return $vparams;
}
/**
* Return Choice constraint choices.
*
* @return array
*
* @throws ConstraintDefinitionException
*/
protected function getChoices(Constraint $constraint, $className)
{
if ($constraint->callback) {
if (is_callable([$className, $constraint->callback])) {
$choices = call_user_func([$className, $constraint->callback]);
} elseif (is_callable($constraint->callback)) {
$choices = call_user_func($constraint->callback);
} else {
throw new ConstraintDefinitionException('The Choice constraint expects a valid callback');
}
} else {
$choices = $constraint->choices;
}
return $choices;
}
}

View file

@ -1,85 +0,0 @@
<?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\Parser;
use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface;
/**
* Uses the Symfony Validation component to extract information about API objects. This is a backwards-compatible Validation component for Symfony2.1
*/
class ValidationParserLegacy extends ValidationParser
{
/**
* @var ClassMetadataFactoryInterface
*/
protected $factory;
/**
* Requires a validation MetadataFactory.
*
* @param MetadataFactoryInterface $factory
*/
public function __construct(ClassMetadataFactoryInterface $factory)
{
$this->factory = $factory;
}
public function supports(array $input)
{
$className = $input['class'];
return null !== $this->factory->getClassMetadata($className);
}
public function parse(array $input)
{
$params = [];
$className = $input['class'];
$classdata = $this->factory->getClassMetadata($className);
$properties = $classdata->getConstrainedProperties();
$refl = $classdata->getReflectionClass();
$defaults = $refl->getDefaultProperties();
foreach ($properties as $property) {
$vparams = [];
$vparams['default'] = $defaults[$property] ?? null;
$pds = $classdata->getMemberMetadatas($property);
foreach ($pds as $propdata) {
$constraints = $propdata->getConstraints();
foreach ($constraints as $constraint) {
$vparams = $this->parseConstraint($constraint, $vparams, $className);
}
}
if (isset($vparams['format'])) {
$vparams['format'] = implode(', ', $vparams['format']);
}
foreach (['dataType', 'readonly', 'required'] as $reqprop) {
if (!isset($vparams[$reqprop])) {
$vparams[$reqprop] = null;
}
}
$params[$property] = $vparams;
}
return $params;
}
}

265
README.md
View file

@ -1,49 +1,258 @@
NelmioApiDocBundle NelmioApiDocBundle
================== ==================
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation [![Build Status](https://secure.travis-ci.org/nelmio/NelmioApiDocBundle.png?branch=master)](http://travis-ci.org/nelmio/NelmioApiDocBundle)
for your APIs.
Documentation The **NelmioApiDocBundle** bundle allows you to generate a decent documentation for your APIs.
-------------
[Read the documentation on symfony.com](https://symfony.com/doc/current/bundles/NelmioApiDocBundle/index.html) **Important:** This bundle is developed in sync with [symfony's repository](https://github.com/symfony/symfony).
For Symfony 2.0.x, you need to use the 1.* version of the bundle.
Contributing ## Installation ##
------------
See Add this bundle to your `composer.json` file:
[CONTRIBUTING](https://github.com/nelmio/NelmioApiDocBundle/blob/master/CONTRIBUTING.md)
file. {
"require": {
"nelmio/api-doc-bundle": "dev-master"
}
}
Register the bundle in `app/AppKernel.php`:
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
);
}
Import the routing definition in `routing.yml`:
# app/config/routing.yml
NelmioApiDocBundle:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /api/doc
Enable the bundle's configuration in `app/config/config.yml`:
# app/config/config.yml
nelmio_api_doc: ~
Running the Tests ## Usage ##
-----------------
Install the [Composer](http://getcomposer.org/) `dev` dependencies: The main problem with documentation is to keep it up to date. That's why the **NelmioApiDocBundle**
uses introspection a lot. Thanks to an annotation, it's really easy to document an API method.
php composer.phar install --dev ### The ApiDoc() annotation ###
Then, run the test suite using The bundle provides an `ApiDoc()` annotation for your controllers:
[PHPUnit](https://github.com/sebastianbergmann/phpunit/):
phpunit ``` php
<?php
namespace Your\Namespace;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
class YourController extends Controller
{
/**
* This the documentation description of your method, it will appear
* on a specific pane. It will read all the text until the first
* annotation.
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* filters={
* {"name"="a-filter", "dataType"="integer"},
* {"name"="another-filter", "dataType"="string", "pattern"="(foo|bar) ASC|DESC"}
* }
* )
*/
public function getAction()
{
}
/**
* @ApiDoc(
* description="Create a new Object",
* input="Your\Namespace\Form\Type\YourType",
* output="Your\Namespace\Class"
* )
*/
public function postAction()
{
}
}
```
The following properties are available:
* `section`: allow to group resources
* `resource`: whether the method describes a main resource or not (default: `false`);
* `description`: a description of the API method;
* `deprecated`: allow to set method as deprecated (default: `false`);
* `filters`: an array of filters;
* `input`: the input type associated to the method, currently this supports Form Types, and classes with JMS Serializer
metadata, useful for POST|PUT methods, either as FQCN or as form type (if it is registered in the form factory in the container)
* `output`: the output type associated with the response. Specified and parsed the same way as `input`.
* `statusCodes`: an array of HTTP status codes and a description of when that status is returned; Example:
``` php
<?php
class YourController
{
/**
* @ApiDoc(
* statusCodes={
* 200="Returned when successful",
* 403="Returned when the user is not authorized to say hello",
* 404={
* "Returned when the user is not found",
* "Returned when somehting else is not found"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
```
Each _filter_ has to define a `name` parameter, but other parameters are free. Filters are often optional
parameters, and you can document them as you want, but keep in mind to be consistent for the whole documentation.
If you set `input`, then the bundle automatically extracts parameters based on the given type,
and determines for each parameter its data type, and if it's required or not.
For Form Types, you can add an extra option named `description` on each field:
For classes parsed with JMS metadata, description will be taken from the properties doc comment, if available.
``` php
<?php
class YourType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('note', null, array(
'description' => 'this is a note',
));
// ...
}
}
```
The bundle will also get information from the routing definition (`requirements`, `pattern`, etc), so to get the
best out of it you should define strict _method requirements etc.
Credits ### Documentation on-the-fly ###
-------
The design is heavily inspired by the By calling an URL with the parameter `?_doc=1`, you will get the corresponding documentation if available.
[swagger-ui](https://github.com/wordnik/swagger-ui) project.
Some icons from the [Glyphicons](http://glyphicons.com/) library are used to
render the documentation.
License ### Web Interface ###
-------
This bundle is released under the MIT license. See the complete license in the You can browse the whole documentation at: `http://example.org/api/doc`.
bundle:
Resources/meta/LICENSE ![](https://github.com/nelmio/NelmioApiDocBundle/raw/master/Resources/doc/webview.png)
![](https://github.com/nelmio/NelmioApiDocBundle/raw/master/Resources/doc/webview2.png)
### Command ###
A command is provided in order to dump the documentation in `json`, `markdown`, or `html`.
php app/console api:doc:dump [--format="..."]
The `--format` option allows to choose the format (default is: `markdown`).
For example to generate a static version of your documentation you can use:
php app/console api:doc:dump --format=html > api.html
By default, the generated HTML will add the sandbox feature if you didn't disable it in the configuration.
If you want to generate a static version of your documentation without sandbox, use the `--no-sandbox` option.
## Configuration ##
You can specify your own API name:
# app/config/config.yml
nelmio_api_doc:
name: My API
This bundle provides a sandbox mode in order to test API methods. You can
configure this sandbox using the following parameters:
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication: # default null, if set, the value of the api key is read from the query string and appended to every sandbox api call
name: access_token
delivery: query # query or http_basic are supported
custom_endpoint: true # default false, if true, your user will be able to specify its own endpoint
enabled: true # default: true, you can set this parameter to `false` to disable the sandbox
endpoint: http://sandbox.example.com/ # default: /app_dev.php, use this parameter to define which URL to call through the sandbox
accept_type: application/json # default null, if set, the value is automatically populated as the Accept header
The bundle provides a way to register multiple `input` parsers. The first parser that can handle the specified
input is used, so you can configure their priorities via container tags. Here's an example parser service registration:
#app/config/config.yml
services:
mybundle.api_doc.extractor.custom_parser:
class: MyBundle\Parser\CustomDocParser
tags:
- { name: nelmio_api_doc.extractor.parser, priority: 2 }
You can also define your own motd content (above methods list). All you have to do is add to configuration:
#app/config/config.yml
motd:
template: AcmeApiBundle::Components/motd.html.twig
## Using Your Own Annotations ##
If you have developed your own project-related annotations, and you want to parse them to populate
the `ApiDoc`, you can provide custom handlers as services. You juste have to implement the
`Nelmio\ApiDocBundle\Extractor\HandlerInterface` and tag it as `nelmio_api_doc.extractor.handler`:
# app/config/config.yml
services:
mybundle.api_doc.extractor.my_annotation_handler:
class: MyBundle\AnnotationHandler\MyAnnotationHandler
tags:
- { name: nelmio_api_doc.extractor.handler }
Look at the built-in [Handlers](https://github.com/nelmio/NelmioApiDocBundle/tree/master/Extractor/Handler).
## Credits ##
The design is heavily inspired by the [swagger-ui](https://github.com/wordnik/swagger-ui) project.
Some icons from the [Glyphicons](http://glyphicons.com/) library are used to render the documentation.

View file

@ -1,23 +0,0 @@
services:
_defaults:
public: false
autowire: true
autoconfigure: true
Nelmio\ApiDocBundle\Command\DumpCommand:
arguments:
$simpleFormatter: '@nelmio_api_doc.formatter.simple_formatter'
$markdownFormatter: '@nelmio_api_doc.formatter.markdown_formatter'
$htmlFormatter: '@nelmio_api_doc.formatter.html_formatter'
$apiDocExtractor: '@nelmio_api_doc.extractor.api_doc_extractor'
Nelmio\ApiDocBundle\Command\SwaggerDumpCommand:
arguments:
$extractor: '@nelmio_api_doc.extractor.api_doc_extractor'
$formatter: '@nelmio_api_doc.formatter.swagger_formatter'
Nelmio\ApiDocBundle\Controller\ApiDocController:
arguments:
$extractor: '@nelmio_api_doc.extractor.api_doc_extractor'
$htmlFormatter: '@nelmio_api_doc.formatter.html_formatter'
$swaggerFormatter: '@nelmio_api_doc.formatter.swagger_formatter'

View file

@ -4,24 +4,29 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters> <parameters>
<parameter key="nelmio_api_doc.parser.form_type_parser.class">Nelmio\ApiDocBundle\Parser\FormTypeParser</parameter>
<parameter key="nelmio_api_doc.formatter.abstract_formatter.class">Nelmio\ApiDocBundle\Formatter\AbstractFormatter</parameter> <parameter key="nelmio_api_doc.formatter.abstract_formatter.class">Nelmio\ApiDocBundle\Formatter\AbstractFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.markdown_formatter.class">Nelmio\ApiDocBundle\Formatter\MarkdownFormatter</parameter> <parameter key="nelmio_api_doc.formatter.markdown_formatter.class">Nelmio\ApiDocBundle\Formatter\MarkdownFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.simple_formatter.class">Nelmio\ApiDocBundle\Formatter\SimpleFormatter</parameter> <parameter key="nelmio_api_doc.formatter.simple_formatter.class">Nelmio\ApiDocBundle\Formatter\SimpleFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.html_formatter.class">Nelmio\ApiDocBundle\Formatter\HtmlFormatter</parameter> <parameter key="nelmio_api_doc.formatter.html_formatter.class">Nelmio\ApiDocBundle\Formatter\HtmlFormatter</parameter>
<parameter key="nelmio_api_doc.formatter.swagger_formatter.class">Nelmio\ApiDocBundle\Formatter\SwaggerFormatter</parameter>
<parameter key="nelmio_api_doc.sandbox.authentication">null</parameter> <parameter key="nelmio_api_doc.sandbox.authentication">null</parameter>
</parameters> </parameters>
<services> <services>
<service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" abstract="true" /> <service id="nelmio_api_doc.parser.form_type_parser" class="%nelmio_api_doc.parser.form_type_parser.class%">
<argument type="service" id="form.factory" />
<argument type="service" id="form.registry" />
<tag name="nelmio_api_doc.extractor.parser" />
</service>
<service id="nelmio_api_doc.formatter.abstract_formatter" class="%nelmio_api_doc.formatter.abstract_formatter.class%" />
<service id="nelmio_api_doc.formatter.markdown_formatter" class="%nelmio_api_doc.formatter.markdown_formatter.class%" <service id="nelmio_api_doc.formatter.markdown_formatter" class="%nelmio_api_doc.formatter.markdown_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" /> parent="nelmio_api_doc.formatter.abstract_formatter" />
<service id="nelmio_api_doc.formatter.simple_formatter" class="%nelmio_api_doc.formatter.simple_formatter.class%" <service id="nelmio_api_doc.formatter.simple_formatter" class="%nelmio_api_doc.formatter.simple_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" /> parent="nelmio_api_doc.formatter.abstract_formatter" />
<service id="nelmio_api_doc.formatter.html_formatter" class="%nelmio_api_doc.formatter.html_formatter.class%" <service id="nelmio_api_doc.formatter.html_formatter" class="%nelmio_api_doc.formatter.html_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" public="true"> parent="nelmio_api_doc.formatter.abstract_formatter">
<call method="setTemplatingEngine"> <call method="setTemplatingEngine">
<argument type="service" id="twig" /> <argument type="service" id="templating" />
</call> </call>
<call method="setMotdTemplate"> <call method="setMotdTemplate">
<argument>%nelmio_api_doc.motd.template%</argument> <argument>%nelmio_api_doc.motd.template%</argument>
@ -38,30 +43,16 @@
<call method="setRequestFormatMethod"> <call method="setRequestFormatMethod">
<argument>%nelmio_api_doc.sandbox.request_format.method%</argument> <argument>%nelmio_api_doc.sandbox.request_format.method%</argument>
</call> </call>
<call method="setRequestFormats">
<argument>%nelmio_api_doc.sandbox.request_format.formats%</argument>
</call>
<call method="setDefaultRequestFormat"> <call method="setDefaultRequestFormat">
<argument>%nelmio_api_doc.sandbox.request_format.default_format%</argument> <argument>%nelmio_api_doc.sandbox.request_format.default_format%</argument>
</call> </call>
<call method="setAcceptType"> <call method="setAcceptType">
<argument>%nelmio_api_doc.sandbox.accept_type%</argument> <argument>%nelmio_api_doc.sandbox.accept_type%</argument>
</call> </call>
<call method="setBodyFormats">
<argument>%nelmio_api_doc.sandbox.body_format.formats%</argument>
</call>
<call method="setDefaultBodyFormat">
<argument>%nelmio_api_doc.sandbox.body_format.default_format%</argument>
</call>
<call method="setAuthentication"> <call method="setAuthentication">
<argument>%nelmio_api_doc.sandbox.authentication%</argument> <argument>%nelmio_api_doc.sandbox.authentication%</argument>
</call> </call>
<call method="setDefaultSectionsOpened">
<argument>%nelmio_api_doc.default_sections_opened%</argument>
</call>
</service> </service>
<service id="nelmio_api_doc.formatter.swagger_formatter" class="%nelmio_api_doc.formatter.swagger_formatter.class%"
parent="nelmio_api_doc.formatter.abstract_formatter" public="true" />
</services> </services>
</container> </container>

View file

@ -1,4 +1,5 @@
nelmio_api_doc_index: nelmio_api_doc_index:
path: /{view} pattern: /
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::index, view: 'default' } defaults: { _controller: NelmioApiDocBundle:ApiDoc:index }
methods: [GET] requirements:
_method: GET

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://nelmio.github.io/schema/dic/nelmio_api_doc"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://nelmio.github.io/schema/dic/nelmio_api_doc"
elementFormDefault="qualified">
<xsd:element name="config">
<xsd:complexType>
<xsd:all>
<xsd:element name="motd" type="motd" minOccurs="0" maxOccurs="1"/>
<xsd:element name="request_listener" type="request_listener" minOccurs="0" maxOccurs="1"/>
<xsd:element name="sandbox" type="sandbox" minOccurs="0" maxOccurs="1"/>
</xsd:all>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="motd">
<xsd:attribute name="template" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="request_listener">
<xsd:attribute name="enabled" default="true" type="xsd:boolean"/>
<xsd:attribute name="parameter" default="_doc" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="sandbox">
<xsd:attribute name="enabled" default="true" type="xsd:boolean"/>
<xsd:attribute name="endpoint" type="xsd:string"/>
<xsd:attribute name="accept_type" type="xsd:string"/>
<xsd:attribute name="body_format" type="body_format_enum"/>
<xsd:attribute name="request_format" type="request_format"/>
<xsd:attribute name="authentication" type="authentication"/>
</xsd:complexType>
<xsd:simpleType name="body_format_enum">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="form"/>
<xsd:enumeration value="json"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="request_format">
<xsd:attribute name="method" type="request_format_method_enum"/>
<xsd:attribute name="default_format" type="request_format_default_format_enum"/>
</xsd:complexType>
<xsd:simpleType name="request_format_method_enum">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="format_param"/>
<xsd:enumeration value="accept_header"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="request_format_default_format_enum">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="json"/>
<xsd:enumeration value="xml"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="authentication">
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="delivery" type="authentication_delivery_enum"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="custom_endpoint" type="xsd:boolean" default="false"/>
</xsd:complexType>
<xsd:simpleType name="authentication_delivery_enum">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="query"/>
<xsd:enumeration value="http_basic"/>
<xsd:enumeration value="header"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="nelmio_api_doc.parser.form_type_parser.class">Nelmio\ApiDocBundle\Parser\FormTypeParser</parameter>
</parameters>
<services>
<service id="nelmio_api_doc.parser.form_type_parser" class="%nelmio_api_doc.parser.form_type_parser.class%">
<argument type="service" id="form.factory" />
<argument type="service" id="translator" />
<argument>%nelmio_api_doc.sandbox.entity_to_choice%</argument>
<tag name="nelmio_api_doc.extractor.parser" />
</service>
</services>
</container>

View file

@ -1,17 +0,0 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="nelmio_api_doc.parser.validation_parser.class">Nelmio\ApiDocBundle\Parser\ValidationParser</parameter>
</parameters>
<services>
<service id="nelmio_api_doc.parser.validation_parser" class="%nelmio_api_doc.parser.validation_parser.class%">
<argument type="service" id="validator.mapping.class_metadata_factory" />
<tag name="nelmio_api_doc.extractor.parser" />
</service>
</services>
</container>

View file

@ -9,29 +9,24 @@
<parameter key="nelmio_api_doc.twig.extension.extra_markdown.class">Nelmio\ApiDocBundle\Twig\Extension\MarkdownExtension</parameter> <parameter key="nelmio_api_doc.twig.extension.extra_markdown.class">Nelmio\ApiDocBundle\Twig\Extension\MarkdownExtension</parameter>
<parameter key="nelmio_api_doc.doc_comment_extractor.class">Nelmio\ApiDocBundle\Util\DocCommentExtractor</parameter> <parameter key="nelmio_api_doc.doc_comment_extractor.class">Nelmio\ApiDocBundle\Util\DocCommentExtractor</parameter>
<parameter key="nelmio_api_doc.extractor.handler.phpdoc.class">Nelmio\ApiDocBundle\Extractor\Handler\PhpDocHandler</parameter> <parameter key="nelmio_api_doc.extractor.handler.fos_rest.class">Nelmio\ApiDocBundle\Extractor\Handler\FosRestHandler</parameter>
<parameter key="nelmio_api_doc.extractor.handler.jms_security.class">Nelmio\ApiDocBundle\Extractor\Handler\JmsSecurityExtraHandler</parameter>
<parameter key="nelmio_api_doc.parser.collection_parser.class">Nelmio\ApiDocBundle\Parser\CollectionParser</parameter> <parameter key="nelmio_api_doc.extractor.handler.sensio_framework_extra.class">Nelmio\ApiDocBundle\Extractor\Handler\SensioFrameworkExtraHandler</parameter>
<parameter key="nelmio_api_doc.parser.form_errors_parser.class">Nelmio\ApiDocBundle\Parser\FormErrorsParser</parameter>
<parameter key="nelmio_api_doc.parser.json_serializable_parser.class">Nelmio\ApiDocBundle\Parser\JsonSerializableParser</parameter>
</parameters> </parameters>
<services> <services>
<service id="nelmio_api_doc.doc_comment_extractor" class="%nelmio_api_doc.doc_comment_extractor.class%" /> <service id='nelmio_api_doc.doc_comment_extractor' class='%nelmio_api_doc.doc_comment_extractor.class%' />
<service id="nelmio_api_doc.controller_name_parser" class="Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser" public="false"> <service id="nelmio_api_doc.extractor.api_doc_extractor" class="%nelmio_api_doc.extractor.api_doc_extractor.class%">
<argument type="service" id="kernel" /> <argument type="service" id="service_container"/>
</service>
<service id="nelmio_api_doc.extractor.api_doc_extractor" class="%nelmio_api_doc.extractor.api_doc_extractor.class%" public="true">
<argument type="service" id="router" /> <argument type="service" id="router" />
<argument type="service" id="annotation_reader" />
<argument type="service" id="nelmio_api_doc.doc_comment_extractor" /> <argument type="service" id="nelmio_api_doc.doc_comment_extractor" />
<argument type="collection" /> <argument type="collection"/>
<argument>%nelmio_api_doc.exclude_sections%</argument>
</service> </service>
<service id="nelmio_api_doc.form.extension.description_form_type_extension" class="%nelmio_api_doc.form.extension.description_form_type_extension.class%"> <service id="nelmio_api_doc.form.extension.description_form_type_extension" class="%nelmio_api_doc.form.extension.description_form_type_extension.class%">
<tag name="form.type_extension" alias="form" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" /> <tag name="form.type_extension" alias="form" />
</service> </service>
<service id="nelmio_api_doc.twig.extension.extra_markdown" class="%nelmio_api_doc.twig.extension.extra_markdown.class%"> <service id="nelmio_api_doc.twig.extension.extra_markdown" class="%nelmio_api_doc.twig.extension.extra_markdown.class%">
@ -40,22 +35,18 @@
<!-- Extractor Annotation Handlers --> <!-- Extractor Annotation Handlers -->
<service id="nelmio_api_doc.extractor.handler.phpdoc" class="%nelmio_api_doc.extractor.handler.phpdoc.class%" public="false"> <service id="nelmio_api_doc.extractor.handler.fos_rest" class="%nelmio_api_doc.extractor.handler.fos_rest.class%" public="false">
<argument type="service" id="nelmio_api_doc.doc_comment_extractor" />
<tag name="nelmio_api_doc.extractor.handler"/> <tag name="nelmio_api_doc.extractor.handler"/>
</service> </service>
<service id="nelmio_api_doc.parser.collection_parser" class="%nelmio_api_doc.parser.collection_parser.class%"> <service id="nelmio_api_doc.extractor.handler.jms_security" class="%nelmio_api_doc.extractor.handler.jms_security.class%" public="false">
<tag name="nelmio_api_doc.extractor.parser" /> <tag name="nelmio_api_doc.extractor.handler"/>
</service>
<service id="nelmio_api_doc.parser.form_errors_parser" class="%nelmio_api_doc.parser.form_errors_parser.class%">
<tag name="nelmio_api_doc.extractor.parser" />
</service> </service>
<!-- priority=1 means it comes before the validation parser, which can often add better type information --> <service id="nelmio_api_doc.extractor.handler.sensio_framework_extra" class="%nelmio_api_doc.extractor.handler.sensio_framework_extra.class%" public="false">
<service id="nelmio_api_doc.parser.json_serializable_parser" class="%nelmio_api_doc.parser.json_serializable_parser.class%"> <tag name="nelmio_api_doc.extractor.handler"/>
<tag name="nelmio_api_doc.extractor.parser" priority="1" />
</service> </service>
</services> </services>
</container> </container>

View file

@ -1,9 +0,0 @@
nelmio_api_doc_swagger_resource_list:
path: /
methods: [GET]
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::swagger }
nelmio_api_doc_swagger_api_declaration:
path: /{resource}
methods: [GET]
defaults: { _controller: Nelmio\ApiDocBundle\Controller\ApiDocController::swagger }

View file

@ -1,21 +0,0 @@
Commands
========
A command is provided in order to dump the documentation in ``json``, ``markdown``,
or ``html``.
.. code-block:: bash
$ php app/console api:doc:dump [--format="..."]
The ``--format`` option allows to choose the format (default is: ``markdown``).
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 didn't
disable it in the configuration. If you want to generate a static version of
your documentation without sandbox, use the ``--no-sandbox`` option.

View file

@ -1,132 +0,0 @@
Configuration In-Depth
======================
API Name
--------
You can specify your own API name:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
name: My API
Authentication Methods
----------------------
You can choose between different authentication methods:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: header
name: X-Custom
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: query
name: param
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication:
delivery: http
type: basic # or bearer
When choosing an ``http`` delivery, ``name`` defaults to ``Authorization``, and
the header value will automatically be prefixed by the corresponding type (ie.
``Basic`` or ``Bearer``).
Section Exclusion
-----------------
You can specify which sections to exclude from the documentation generation:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
exclude_sections: ["privateapi", "testapi"]
Note that ``exclude_sections`` will literally exclude a section from your api
documentation. It's possible however to create multiple views by specifying the
``views`` parameter within the ``@ApiDoc`` annotations. This allows you to move
private or test methods to a complete different view of your documentation
instead.
Parsers
-------
By default, all registered parsers are used, but sometimes you may want to
define which parsers you want to use. The ``parsers`` attribute is used to
configure a list of parsers that will be used::
output={
"class" = "Acme\Bundle\Entity\User",
"parsers" = {
"Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
"Nelmio\ApiDocBundle\Parser\ValidationParser"
}
}
In this case the parsers ``JmsMetadataParser`` and ``ValidationParser`` are used
to generate returned data. This feature also works for both the ``input`` and
``output`` properties.
Moreover, the bundle provides a way to register multiple ``input`` parsers. The
first parser that can handle the specified input is used, so you can configure
their priorities via container tags. Here's an example parser service
registration:
.. code-block:: yaml
# app/config/config.yml
services:
mybundle.api_doc.extractor.custom_parser:
class: MyBundle\Parser\CustomDocParser
tags:
- { name: nelmio_api_doc.extractor.parser, priority: 2 }
MOTD
----
You can also define your own motd content (above methods list). All you have to
do is add to configuration:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
# ...
motd:
template: AcmeApiBundle::Components/motd.html.twig
Caching
-------
It is a good idea to enable the internal caching mechanism on production:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
cache:
enabled: true
You can define an alternate location where the ApiDoc configurations are to be
cached:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
cache:
enabled: true
file: "/tmp/symfony-app/%kernel.environment%/api-doc.cache"

View file

@ -1,55 +0,0 @@
Configuration Reference
=======================
.. code-block:: yaml
nelmio_api_doc:
name: 'API documentation'
exclude_sections: []
default_sections_opened: true
motd:
template: '@NelmioApiDoc/Components/motd.html.twig'
request_listener:
enabled: true
parameter: _doc
sandbox:
enabled: true
endpoint: null
accept_type: null
body_format:
formats:
# Defaults:
- form
- json
default_format: ~ # One of "form"; "json"
request_format:
formats:
# Defaults:
json: application/json
xml: application/xml
method: ~ # One of "format_param"; "accept_header"
default_format: json
authentication:
delivery: ~ # Required
name: ~ # Required
# Required if http delivery is selected.
type: ~ # One of "basic"; "bearer"
custom_endpoint: false
entity_to_choice: true
swagger:
api_base_path: /api
swagger_version: '1.2'
api_version: '0.1'
info:
title: Symfony2
description: 'My awesome Symfony2 app!'
TermsOfServiceUrl: null
contact: null
license: null
licenseUrl: null
cache:
enabled: false
file: '%kernel.cache_dir%/api-doc.cache'

View file

@ -1,12 +0,0 @@
Frequently Asked Questions
==========================
How can I remove the parameter ``_format`` sent in ``POST`` and ``PUT`` request?
--------------------------------------------------------------------------------
.. code-block:: yaml
nelmio_api_doc:
sandbox:
request_format:
method: accept_header

View file

@ -1,143 +0,0 @@
NelmioApiDocBundle
==================
The **NelmioApiDocBundle** bundle allows you to generate a decent documentation
for your APIs.
Installation
------------
Step 1: Download the Bundle
---------------------------
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
.. code-block:: bash
$ composer require nelmio/api-doc-bundle
This command requires you to have Composer installed globally, as explained
in the `installation chapter`_ of the Composer documentation.
Step 2: Enable the Bundle
-------------------------
Then, enable the bundle by adding it to the list of registered bundles
in the ``app/AppKernel.php`` file of your project:
.. code-block:: php
<?php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
);
// ...
}
// ...
}
Step 3: Register the Routes
---------------------------
Import the routing definition in ``routing.yml``:
.. code-block:: yaml
# app/config/routing.yml
NelmioApiDocBundle:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /api/doc
Step 4: Configure the Bundle
----------------------------
Enable the bundle's configuration in ``app/config/config.yml``:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc: ~
The **NelmioApiDocBundle** requires Twig as a template engine so do not forget
to enable it:
.. code-block:: yaml
# app/config/config.yml
framework:
templating:
engines: ['twig']
Usage
-----
The main problem with documentation is to keep it up to date. That's why the
**NelmioApiDocBundle** uses introspection a lot. Thanks to an annotation, it's
really easy to document an API method. The following chapters will help you
setup your API documentation:
.. toctree::
:maxdepth: 1
the-apidoc-annotation
multiple-api-doc
other-bundle-annotations
swagger-support
sandbox
commands
configuration-in-depth
configuration-reference
faq
Web Interface
~~~~~~~~~~~~~
You can browse the whole documentation at: ``http://example.org/api/doc``.
.. image:: webview.png
:align: center
.. image:: webview2.png
:align: center
On-The-Fly Documentation
~~~~~~~~~~~~~~~~~~~~~~~~
By calling an URL with the parameter ``?_doc=1``, you will get the corresponding
documentation if available.
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
Route versions
~~~~~~~~~~~~~~
You can define version for the API routes:
.. code-block:: yaml
api_v3_products_list:
pattern: /api/v3/products.{_format}
defaults: { _controller: NelmioApiDocTestBundle:Test:routeVersion, _format: json, _version: "3.0" }
requirements:
_method: GET
api_v1_orders:
resource: "@AcmeOrderBundle/Resources/config/routing/orders_v1.yml"
defaults: { _version: "1.0" }
prefix: /api/v1/orders
And generate documentation for specific version by the command:
.. code-block:: bash
php app/console api:doc:dump --format=html --api-version=3.0 > api.html
Or by adding `?_version={version}` to API documentation page URL.

View file

@ -1,59 +0,0 @@
Multiple API Documentation ("Views")
====================================
With the ``views`` tag in the ``@ApiDoc`` annotation, it is possible to create
different views of your API documentation. Without the tag, all methods are
located in the ``default`` view, and can be found under the normal API
documentation url.
You can specify one or more *view* names under which the method will be
visible.
An example::
/**
* A resource
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* views = { "default", "premium" }
* )
*/
public function getAction()
{
}
/**
* Another resource
*
* @ApiDoc(
* resource=true,
* description="This is a description of another API method",
* views = { "premium" }
* )
*/
public function getAnotherAction()
{
}
In this case, only the first resource will be available under the default view,
while both methods will be available under the ``premium`` view.
Accessing Specific API Views
----------------------------
The ``default`` view can be found at the normal location. Other views can be
found at ``http://your.documentation/<view name>``.
For instance, if your documentation is located at:
.. code-block:: text
http://example.org/doc/api/v1/
then the ``premium`` view will be located at:
.. code-block:: text
http://example.org/doc/api/v1/premium

View file

@ -1,81 +0,0 @@
Other Bundle Annotations
========================
PHPDoc
------
Actions marked as ``@deprecated`` will be marked as such in the interface.
JMS Serializer Features
-----------------------
The bundle has support for some of the JMS Serializer features and uses this
extra piece of information in the generated documentation.
Group Exclusion Strategy
------------------------
If your classes use `JMS Group Exclusion Strategy`_, you can specify which
groups to use when generating the documentation by using this syntax::
input={
"class"="Acme\Bundle\Entity\User",
"groups"={"update", "public"}
}
In this case the groups ``update`` and ``public`` are used. This feature also
works for the ``output`` property.
Versioning Objects
------------------
If your ``output`` classes use `versioning capabilities of JMS Serializer`_, the
versioning information will be automatically used when generating the
documentation.
Form Types Features
-------------------
Even if you use ``FormFactoryInterface::createNamed('', 'your_form_type')`` the
documentation will generate the form type name as the prefix for inputs
(``your_form_type[param]`` ... instead of just ``param``).
You can specify which prefix to use with the ``name`` key in the ``input``
section::
input = {
"class" = "your_form_type",
"name" = ""
}
You can also add some options to pass to the form. You just have to use the
``options`` key::
input = {
"class" = "your_form_type",
"options" = {"method" = "PUT"},
}
Using Your Own Annotations
--------------------------
If you have developed your own project-related annotations, and you want to
parse them to populate the ``ApiDoc``, you can provide custom handlers as
services. You just have to implement the
``Nelmio\ApiDocBundle\Extractor\HandlerInterface`` and tag it as
``nelmio_api_doc.extractor.handler``:
.. code-block:: yaml
# app/config/config.yml
services:
mybundle.api_doc.extractor.my_annotation_handler:
class: MyBundle\AnnotationHandler\MyAnnotationHandler
tags:
- { name: nelmio_api_doc.extractor.handler }
Look at the `built-in Handlers`_.
.. _`JMS Group Exclusion Strategy`: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#creating-different-views-of-your-objects
.. _`versioning capabilities of JMS Serializer`: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects
.. _`built-in Handlers`: https://github.com/nelmio/NelmioApiDocBundle/tree/master/Extractor/Handler

View file

@ -1,54 +0,0 @@
Sandbox
=======
This bundle provides a sandbox mode in order to test API methods. You can
configure this sandbox using the following parameters:
.. code-block:: yaml
# app/config/config.yml
nelmio_api_doc:
sandbox:
authentication: # default is `~` (`null`), if set, the sandbox automatically
# send authenticated requests using the configured `delivery`
name: access_token # access token name or query parameter name or header name
delivery: http # `query`, `http`, and `header` are supported
# Required if http delivery is selected.
type: basic # `basic`, `bearer` are supported
custom_endpoint: true # default is `false`, if `true`, your user will be able to
# specify its own endpoint
enabled: true # default is `true`, you can set this parameter to `false`
# to disable the sandbox
endpoint: http://sandbox.example.com/ # default is `/app_dev.php`, use this parameter
# to define which URL to call through the sandbox
accept_type: application/json # default is `~` (`null`), if set, the value is
# automatically populated as the `Accept` header
body_format:
formats: [ form, json ] # array of enabled body formats,
# remove all elements to disable the selectbox
default_format: form # default is `form`, determines whether to send
# `x-www-form-urlencoded` data or json-encoded
# data (by setting this parameter to `json`) in
# sandbox requests
request_format:
formats: # default is `json` and `xml`,
json: application/json # override to add custom formats or disable
xml: application/xml # the default formats
method: format_param # default is `format_param`, alternately `accept_header`,
# decides how to request the response format
default_format: json # default is `json`,
# default content format to request (see formats)
entity_to_choice: false # default is `true`, if `false`, entity collection
# will not be mapped as choice

View file

@ -1,163 +0,0 @@
Swagger Support
===============
It is possible to make your application produce Swagger-compliant JSON output
based on ``@ApiDoc`` annotations, which can be used for consumption by
`swagger-ui`_.
Annotation options
------------------
A couple of properties has been added to ``@ApiDoc``:
To define a **resource description**::
/**
* @ApiDoc(
* resource=true,
* resourceDescription="Operations on users.",
* description="Retrieve list of users."
* )
*/
public function listUsersAction()
{
/* Stuff */
}
The ``resourceDescription`` is distinct from ``description`` as it applies to the
whole resource group and not just the particular API endpoint.
Defining a form-type as a GET form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you use forms to capture GET requests, you will have to specify the
``paramType`` to ``query`` in the annotation::
/**
* @ApiDoc(
* input = {"class" = "Foo\ContentBundle\Form\SearchType", "paramType" = "query"},
* ...
* )
*/
public function searchAction(Request $request)
{
//...
}
Multiple response models
------------------------
Swagger provides you the ability to specify alternate output models for
different status codes. Example, ``200`` would return your default resource object
in JSON form, but ``400`` may return a custom validation error list object. This
can be specified through the ``responseMap`` property::
/**
* @ApiDoc(
* description="Retrieve list of users.",
* statusCodes={
* 400 = "Validation failed."
* },
* responseMap={
* 200 = "FooBundle\Entity\User",
* 400 = {
* "class"="CommonBundle\Model\ValidationErrors",
* "parsers"={"Nelmio\ApiDocBundle\Parser\JmsMetadataParser"}
* }
* }
* )
*/
public function updateUserAction()
{
/* Stuff */
}
This will tell Swagger that ``CommonBundle\Model\ValidationErrors`` is returned
when this endpoint returns a ``400 Validation failed.`` HTTP response.
.. note::
You can omit the ``200`` entry in the ``responseMap`` property and specify
the default ``output`` property instead. That will result on the same thing.
Integration with ``swagger-api/swagger-ui``
-------------------------------------------
You could import the routes for use with `swagger-ui`_.
.. code-block:: yaml
#app/config/routing.yml
nelmio_api_swagger:
resource: "@NelmioApiDocBundle/Resources/config/swagger_routing.yml"
prefix: /api-docs
Et voila!, simply specify http://yourdomain.com/api-docs in your ``swagger-ui``
instance and you are good to go.
.. note::
If your ``swagger-ui`` instance does not live under the same domain, you
will probably encounter some problems related to same-origin policy
violations. `NelmioCorsBundle`_ can solve this problem for you. Read through
how to allow cross-site requests for the ``/api-docs/*`` pages.
Dumping the Swagger-compliant JSON API definitions
--------------------------------------------------
To display all JSON definitions:
.. code-block:: bash
$ php app/console api:swagger:dump
To dump just the resource list:
.. code-block:: bash
$ php app/console api:swagger:dump --list-only
To dump just the API definition the ``users`` resource:
.. code-block:: bash
$ php app/console api:swagger:dump --resource=users
Specify the ``--pretty`` flag to display a prettified JSON output.
Dump to files
~~~~~~~~~~~~~
You can specify the destination if you wish to dump the JSON definition to a file:
.. code-block:: bash
$ php app/console api:swagger:dump --list-only swagger-docs/api-docs.json
$ php app/console api:swagger:dump --resource=users swagger-docs/users.json
Or, you can dump everything into a directory in one command:
.. code-block:: bash
$ php app/console api:swagger:dump swagger-docs
Model naming
------------
By default, the model naming strategy used is the ``dot_notation`` strategy. The
model IDs are simply the Fully Qualified Class Name (FQCN) of the class
associated to it, with the ``\`` replaced with ``.``:
.. code-block:: text
Vendor\UserBundle\Entity\User => Vendor.UserBundle.Entity.User
You can also change the ``model_naming_strategy`` in the configuration to
``last_segment_only``, if you want model IDs to be just the class name minus the
namespaces (``Vendor\UserBundle\Entity\User => User``). This will not afford you
the guarantee that model IDs are unique, but that would really just depend on
the classes you have in use.
.. _`swagger-ui`: https://github.com/swagger-api/swagger-ui
.. _`NelmioCorsBundle`: https://github.com/nelmio/NelmioCorsBundle

View file

@ -1,181 +0,0 @@
The ``ApiDoc()`` Annotation
===========================
The bundle provides an ``ApiDoc()`` annotation for your controllers::
namespace Your\Namespace;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
class YourController extends Controller
{
/**
* This is the documentation description of your method, it will appear
* on a specific pane. It will read all the text until the first
* annotation.
*
* @ApiDoc(
* resource=true,
* description="This is a description of your API method",
* filters={
* {"name"="a-filter", "dataType"="integer"},
* {"name"="another-filter", "dataType"="string", "pattern"="(foo|bar) ASC|DESC"}
* }
* )
*/
public function getAction()
{
}
/**
* @ApiDoc(
* description="Create a new Object",
* input="Your\Namespace\Form\Type\YourType",
* output="Your\Namespace\Class"
* )
*/
public function postAction()
{
}
/**
* @ApiDoc(
* description="Returns a collection of Object",
* requirements={
* {
* "name"="limit",
* "dataType"="integer",
* "requirement"="\d+",
* "description"="how many objects to return"
* }
* },
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="category id"}
* }
* )
*/
public function cgetAction($limit)
{
}
}
The following properties are available:
* ``section``: allow to group resources
* ``resource``: whether the method describes a main resource or not (default:
``false``);
* ``description``: a description of the API method;
* ``deprecated``: allow to set method as deprecated (default: ``false``);
* ``tags``: allow to tag a method (e.g. ``beta`` or ``in-development``). Either
a single tag or an array of tags. Each tag can have an optional hex colorcode
attached.
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* tags={
* "stable",
* "deprecated" = "#ff0000"
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``filters``: an array of filters;
* ``requirements``: an array of requirements;
* ``parameters``: an array of parameters;
* ``headers``: an array of headers; available properties are: ``name``, ``description``, ``required``, ``default``. Example:
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* headers={
* {
* "name"="X-AUTHORIZE-KEY",
* "description"="Authorization key"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``input``: the input type associated to the method (currently this supports
Form Types, classes with JMS Serializer metadata, classes with Validation
component metadata and classes that implement JsonSerializable) useful for
POST|PUT methods, either as FQCN or as form type (if it is registered in the
form factory in the container).
* ``output``: the output type associated with the response. Specified and
parsed the same way as ``input``.
* ``statusCodes``: an array of HTTP status codes and a description of when that
status is returned; Example:
.. code-block:: php
class YourController
{
/**
* @ApiDoc(
* statusCodes={
* 200="Returned when successful",
* 403="Returned when the user is not authorized to say hello",
* 404={
* "Returned when the user is not found",
* "Returned when something else is not found"
* }
* }
* )
*/
public function myFunction()
{
// ...
}
}
* ``views``: the view(s) under which this resource will be shown. Leave empty to
specify the default view. Either a single view, or an array of views.
Each *filter* has to define a ``name`` parameter, but other parameters are free.
Filters are often optional parameters, and you can document them as you want,
but keep in mind to be consistent for the whole documentation.
If you set ``input``, then the bundle automatically extracts parameters based on
the given type, and determines for each parameter its data type, and if it's
required or not.
For classes parsed with JMS metadata, description will be taken from the
properties doc comment, if available.
For Form Types, you can add an extra option named ``description`` on each field::
class YourType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('note', null, array(
'description' => 'this is a note',
));
// ...
}
}
The bundle will also get information from the routing definition
(``requirements``, ``path``, etc), so to get the best out of it you should
define strict methods requirements etc.

View file

@ -99,11 +99,6 @@ em {
code, pre { code, pre {
font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
background-color: #fcf6db; background-color: #fcf6db;
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
} }
p code { p code {
@ -118,20 +113,9 @@ pre {
line-height:1.2em; line-height:1.2em;
} }
div.content ul, div.content ol {
line-height: 1.4em;
color: #333333;
}
table.fullwidth { table.fullwidth {
width: 100%; width: 100%;
} }
table.fullwidth > tbody > tr {
border-bottom: 1px solid #cccccc;
}
table.fullwidth > tbody tr:last-child {
border-bottom: none;
}
table thead tr th { table thead tr th {
padding: 5px; padding: 5px;
font-size: 0.9em; font-size: 0.9em;
@ -141,12 +125,12 @@ table thead tr th {
table tbody tr td { table tbody tr td {
padding: 6px; padding: 6px;
font-size: 0.9em; font-size: 0.9em;
border-bottom: 1px solid #cccccc;
vertical-align: top; vertical-align: top;
line-height: 1.3em; line-height: 1.3em;
} }
table tbody tr:last-child td {
table tbody tr td.format { border-bottom: none;
word-break: break-word;
} }
#header { #header {
@ -185,38 +169,11 @@ table tbody tr td.format {
font-size: 0.9em; font-size: 0.9em;
} }
.section { #section {
padding: 5px 20px; border: 1px solid #ddd;
border-bottom: 1px solid #ddd; background: #f8f8f8;
} padding: 5px 20px;
margin-bottom: 15px;
.section h1 {
padding: 0;
}
.section.active {
border: 1px solid #ddd;
background: #f8f8f8;
margin: 15px 0;
}
.section.active h1 {
padding: 10px 0;
}
.section .actions {
text-align: right;
float: right;
margin-top: 10px;
}
.section .actions a {
cursor: pointer;
margin-left: 10px;
}
.section .actions a:hover {
text-decoration: underline;
} }
li.resource { li.resource {
@ -229,14 +186,14 @@ li.resource:last-child {
} }
/* heading */ /* heading */
.heading { div.heading {
border: 1px solid transparent; border: 1px solid transparent;
float: none; float: none;
clear: both; clear: both;
overflow: hidden; overflow: hidden;
display: block; display: block;
} }
.heading h2 { div.heading h2 {
color: #999999; color: #999999;
padding-left: 0; padding-left: 0;
display: block; display: block;
@ -245,7 +202,7 @@ li.resource:last-child {
font-family: "Droid Sans", sans-serif; font-family: "Droid Sans", sans-serif;
font-weight: bold; font-weight: bold;
} }
.heading ul.options { div.heading ul.options {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
display: block; display: block;
@ -253,7 +210,7 @@ li.resource:last-child {
float: right; float: right;
margin: 6px 10px 0 0; margin: 6px 10px 0 0;
} }
.heading ul.options li { div.heading ul.options li {
float: left; float: left;
clear: none; clear: none;
margin: 0; margin: 0;
@ -262,12 +219,12 @@ li.resource:last-child {
color: #666666; color: #666666;
font-size: 0.9em; font-size: 0.9em;
} }
.heading ul.options li:first-child, div.heading ul.options li:first-child,
.heading ul.options li.first { div.heading ul.options li.first {
padding-left: 0; padding-left: 0;
} }
.heading ul.options li:last-child, div.heading ul.options li:last-child,
.heading ul.options li.last { div.heading ul.options li.last {
padding-right: 0; padding-right: 0;
border-right: none; border-right: none;
} }
@ -281,13 +238,13 @@ li.operation {
margin: 0 0 10px; margin: 0 0 10px;
padding: 0 0 0 0; padding: 0 0 0 0;
} }
li.operation .heading { li.operation div.heading {
margin: 0 0 0 0; margin: 0 0 0 0;
padding: 0; padding: 0;
background-color: #f0f0f0; background-color: #f0f0f0;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
li.operation .heading h3 { li.operation div.heading h3 {
display: block; display: block;
clear: none; clear: none;
float: left; float: left;
@ -297,25 +254,11 @@ li.operation .heading h3 {
line-height: 1.1em; line-height: 1.1em;
color: black; color: black;
} }
li.operation .heading h3 span { li.operation div.heading h3 span {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
li.operation .heading h3 span.icon { li.operation div.heading h3 span.http_method a, li.operation div.heading h3 span.deprecated a {
display: inline-block;
height: 12px;
width: 12px;
margin-left: 3px;
background: no-repeat center center;
}
li.operation .heading h3 span.lock {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAMCAYAAABbayygAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUHEisepJ6ljAAAAJ5JREFUGNNt0LEOAUEUheEPuwkFtSg0old4Eo/imbQKiULpBZQSCtFoaIjSktXMxpjsSW5xzvnnZmb4aYMymg9WEq1Decc1zCNkyxisoFGUTXDGEZpR8cIp8jccKiaLigwDdMP9hughr8ptALtYoB18C+Pgd5KXlrhgX5P/mSfmmKVgM/mmDP1qQ1rEyjFFkYKNmtMF3uikYFGzOdXnC5FWMZNd2GfvAAAAAElFTkSuQmCC");
}
li.operation .heading h3 span.keys {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAMCAYAAAC0qUeeAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUHEisb1PRRAwAAAN9JREFUKM9lz71KQ0EQhuEnJ7EIKhZWaRULu4htLLwDvYRgZWmTWoI3kAuIlVik0U5Io4USRMFOUGwU7cQ/kAQMajMHDpuBZWd235lvPtjHDT4xxhuu0ZJEhhXU8YAG7rCKBWyn8EnkVSxjOuoPbKTT1/GXnCd0YqWt4uQrk3GLGcxiswgPcRgG4QsDzKMSKtUc/kUbFwEf4BlrUdeCk8WOj3jBO+5xhGOMwmQzh6Ec9zemwtgOLuN9D4tZYqwUSvnuu3jFHLpZASqHUqXQfIZe5PX8Y4RTLKGfqLVwjp9/HR4zOkGnnAoAAAAASUVORK5CYII=");
}
li.operation .heading h3 span.http_method i, li.operation .heading h3 span.deprecated i {
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
color: white; color: white;
@ -329,24 +272,12 @@ li.operation .heading h3 span.http_method i, li.operation .heading h3 span.depre
border-radius: 2px; border-radius: 2px;
background-color: #ccc; background-color: #ccc;
} }
li.operation .heading h3 span.deprecated i { li.operation div.heading h3 span.deprecated a {
width: 75px; width: 75px;
background-color: #F00; background-color: #F00;
} }
li.operation .heading h3 span.path { li.operation div.heading h3 span.path {
padding-left: 5px; padding-left: 10px;
}
li.operation .heading h3 span.tag {
color: #FFFFFF;
font-size: 0.7em;
vertical-align: baseline;
background-color: #d9534f;
padding-bottom: 3px;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
border-radius: 4px;
} }
li.operation div.content { li.operation div.content {
@ -374,14 +305,14 @@ li.operation div.content form input[type='text'].error {
} }
/* GET operations */ /* GET operations */
li.operation.get .heading { li.operation.get div.heading {
border-color: #c3d9ec; border-color: #c3d9ec;
background-color: #e7f0f7; background-color: #e7f0f7;
} }
li.operation.get .heading h3 span.http_method i { li.operation.get div.heading h3 span.http_method a {
background-color: #0f6ab4; background-color: #0f6ab4;
} }
li.operation.get .heading ul.options li { li.operation.get div.heading ul.options li {
border-right-color: #c3d9ec; border-right-color: #c3d9ec;
color: #0f6ab4; color: #0f6ab4;
} }
@ -395,14 +326,14 @@ li.operation.get div.content h4 {
} }
/* POST operations */ /* POST operations */
li.operation.post .heading { li.operation.post div.heading {
border-color: #a7e1a1; border-color: #c3e8d1;
background-color: #d4f7cd; background-color: #def1e5;
} }
li.operation.post .heading h3 span.http_method i{ li.operation.post div.heading h3 span.http_method a{
background-color: #10a54a; background-color: #10a54a;
} }
li.operation.post .heading ul.options li { li.operation.post div.heading ul.options li {
border-right-color: #c3e8d1; border-right-color: #c3e8d1;
color: #10a54a; color: #10a54a;
} }
@ -416,14 +347,14 @@ li.operation.post div.content h4 {
} }
/* ANY operations */ /* ANY operations */
li.operation.any .heading { li.operation.any div.heading {
background-color: lightgray; background-color: lightgray;
border-color: gray; border-color: gray;
} }
li.operation.any .heading h3 span.http_method i { li.operation.any div.heading h3 span.http_method a {
background-color: #000; background-color: #000;
} }
li.operation.any .heading ul.options li { li.operation.any div.heading ul.options li {
color: #000; color: #000;
border-right-color: gray; border-right-color: gray;
} }
@ -437,14 +368,14 @@ li.operation.any div.content h4 {
} }
/* PUT operations */ /* PUT operations */
li.operation.put .heading { li.operation.put div.heading {
background-color: #f9f2e9; background-color: #f9f2e9;
border-color: #f0e0ca; border-color: #f0e0ca;
} }
li.operation.put .heading h3 span.http_method i { li.operation.put div.heading h3 span.http_method a {
background-color: #c5862b; background-color: #c5862b;
} }
li.operation.put .heading ul.options li { li.operation.put div.heading ul.options li {
border-right-color: #f0e0ca; border-right-color: #f0e0ca;
color: #c5862b; color: #c5862b;
} }
@ -458,14 +389,14 @@ li.operation.put div.content h4 {
} }
/* DELETE operations */ /* DELETE operations */
li.operation.delete .heading { li.operation.delete div.heading {
background-color: #f5e8e8; background-color: #f5e8e8;
border-color: #e8c6c7; border-color: #e8c6c7;
} }
li.operation.delete .heading h3 span.http_method i { li.operation.delete div.heading h3 span.http_method a {
background-color: #a41e22; background-color: #a41e22;
} }
li.operation.delete .heading ul.options li { li.operation.delete div.heading ul.options li {
border-right-color: #e8c6c7; border-right-color: #e8c6c7;
color: #a41e22; color: #a41e22;
} }
@ -479,14 +410,14 @@ li.operation.delete div.content h4 {
} }
/* PATCH operations */ /* PATCH operations */
li.operation.patch .heading { li.operation.patch div.heading {
background-color: #f5e8e8; background-color: #f5e8e8;
border-color: #e8c6e7; border-color: #e8c6e7;
} }
li.operation.patch .heading h3 span.http_method i { li.operation.patch div.heading h3 span.http_method a {
background-color: #a41ee2; background-color: #a41ee2;
} }
li.operation.patch .heading ul.options li { li.operation.patch div.heading ul.options li {
border-right-color: #e8c6c7; border-right-color: #e8c6c7;
color: #a41ee2; color: #a41ee2;
} }
@ -500,13 +431,13 @@ li.operation.patch div.content h4 {
} }
/* LINK operations */ /* LINK operations */
li.operation.link .heading { li.operation.link div.heading {
background-color: #F7F7D5; background-color: #F7F7D5;
} }
li.operation.link .heading h3 span.http_method i { li.operation.link div.heading h3 span.http_method a {
background-color: #C3D448; background-color: #C3D448;
} }
li.operation.link .heading ul.options li { li.operation.link div.heading ul.options li {
color: #C3D448; color: #C3D448;
} }
@ -518,13 +449,13 @@ li.operation.link div.content h4 {
} }
/* UNLINK operations */ /* UNLINK operations */
li.operation.unlink .heading { li.operation.unlink div.heading {
background-color: #FFEBDE; background-color: #FFEBDE;
} }
li.operation.unlink .heading h3 span.http_method i { li.operation.unlink div.heading h3 span.http_method a {
background-color: #FF8438; background-color: #FF8438;
} }
li.operation.unlink .heading ul.options li { li.operation.unlink div.heading ul.options li {
color: #FF8438; color: #FF8438;
} }
@ -578,6 +509,7 @@ li.operation.unlink div.content h4 {
.pane.sandbox { .pane.sandbox {
border: 1px solid #C3D9EC; border: 1px solid #C3D9EC;
border-top: none;
padding: 10px; padding: 10px;
} }
@ -593,19 +525,11 @@ li.operation.unlink div.content h4 {
display: none; display: none;
} }
form .parameters { form .parameters,
float: left;
width: 50%;
}
form .parameters .tuple input, form .parameters .tuple textarea {
width: 40%;
}
form .headers, form .headers,
form .request-content { form .request-content {
float: left; float: left;
width: 25%; width: 33%;
} }
.buttons { .buttons {
@ -627,11 +551,11 @@ form .request-content {
margin-bottom: 10px; margin-bottom: 10px;
} }
.motd { .icon {
padding:20px; height: 12px;
margin-left: 3px;
} }
.json-collapse-section { .motd {
color: #660; padding:20px;
cursor: pointer; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,8 +0,0 @@
{% if sinceVersion is empty and untilVersion is empty %}
*
{% else %}
{% if sinceVersion is not empty %}&gt;={{ sinceVersion }}{% endif %}
{% if untilVersion is not empty %}
{% if sinceVersion is not empty %},{% endif %}&lt;={{ untilVersion }}
{% endif %}
{% endif %}

View file

@ -14,40 +14,23 @@
</head> </head>
<body> <body>
<div id="header"> <div id="header">
<a href="{{ path('nelmio_api_doc_index') }}"><h1>{{ apiName }}</h1></a> <a href=""><h1>{{ apiName }}</h1></a>
{% if enableSandbox %} <div id="sandbox_configuration">
<div id="sandbox_configuration"> request format:
{% if bodyFormats|length > 0 %} <select id="request_format">
body format: <option value="json"{{ defaultRequestFormat == 'json' ? ' selected' : '' }}>JSON</option>
<select id="body_format"> <option value="xml"{{ defaultRequestFormat == 'xml' ? ' selected' : '' }}>XML</option>
{% if 'form' in bodyFormats %}<option value="form"{{ defaultBodyFormat == 'form' ? ' selected' : '' }}>Form Data</option>{% endif %} </select>
{% if 'json' in bodyFormats %}<option value="json"{{ defaultBodyFormat == 'json' ? ' selected' : '' }}>JSON</option>{% endif %} {% if authentication and authentication.delivery in ['query', 'http_basic'] %}
</select> api key: <input type="text" id="api_key" value=""/>
{% endif %} {% endif %}
{% if requestFormats|length > 0 %} {% if authentication and authentication.delivery in ['http_basic'] %}
request format: api pass: <input type="text" id="api_pass" value=""/>
<select id="request_format"> {% endif %}
{% for format, header in requestFormats %} {% if authentication and authentication.custom_endpoint %}
<option value="{{ header }}"{{ defaultRequestFormat == format ? ' selected' : '' }}>{{ format }}</option> api endpoint: <input type="text" id="api_endpoint" value=""/>
{% endfor %} {% endif %}
{% endif %} </div>
</select>
{% if authentication %}
{% if authentication.delivery == 'http' and authentication.type == 'basic' %}
api login: <input type="text" id="api_login" value=""/>
api password: <input type="text" id="api_pass" value=""/>
{% elseif authentication.delivery in ['query', 'http', 'header'] %}
api key: <input type="text" id="api_key" value=""/>
{% endif %}
{% if authentication.custom_endpoint %}
api endpoint: <input type="text" id="api_endpoint" value=""/>
{% endif %}
<button id="save_api_auth" type="button">Save</button>
<button id="clear_api_auth" type="button">Clear</button>
{% endif %}
</div>
{% endif %}
<br style="clear: both;" /> <br style="clear: both;" />
</div> </div>
{% include motdTemplate %} {% include motdTemplate %}
@ -60,162 +43,11 @@
Documentation auto-generated on {{ date }} Documentation auto-generated on {{ date }}
</p> </p>
<script type="text/javascript"> <script type="text/javascript">
$('.toggler').click(function() {
var getHash = function() { $(this).next().slideToggle('slow');
return window.location.hash || '';
};
var setHash = function(hash) {
window.location.hash = hash;
};
var clearHash = function() {
var scrollTop, scrollLeft;
if(typeof history === 'object' && typeof history.pushState === 'function') {
history.replaceState('', document.title, window.location.pathname + window.location.search);
} else {
scrollTop = document.body.scrollTop;
scrollLeft = document.body.scrollLeft;
setHash('');
document.body.scrollTop = scrollTop;
document.body.scrollLeft = scrollLeft;
}
};
$(window).load(function() {
var id = getHash().substr(1).replace( /([:\.\[\]\{\}|])/g, "\\$1");
var elem = $('#' + id);
if (elem.length) {
setTimeout(function() {
$('body,html').scrollTop(elem.position().top);
});
elem.find('.toggler').click();
var section = elem.parents('.section').first();
if (section) {
section.addClass('active');
section.find('.section-list').slideDown('fast');
}
}
{% if enableSandbox %}
loadStoredAuthParams();
{% endif %}
});
$('.toggler').click(function(event) {
var contentContainer = $(this).next();
if(contentContainer.is(':visible')) {
clearHash();
} else {
setHash($(this).data('href'));
}
contentContainer.slideToggle('fast');
return false;
});
$('.action-show-hide, .section > h1').on('click', function(){
var section = $(this).parents('.section').first();
if (section.hasClass('active')) {
section.removeClass('active');
section.find('.section-list').slideUp('fast');
} else {
section.addClass('active');
section.find('.section-list').slideDown('fast');
}
});
$('.action-list').on('click', function(){
var section = $(this).parents('.section').first();
if (!section.hasClass('active')) {
section.addClass('active');
}
section.find('.section-list').slideDown('fast');
section.find('.operation > .content').slideUp('fast');
});
$('.action-expand').on('click', function(){
var section = $(this).parents('.section').first();
if (!section.hasClass('active')) {
section.addClass('active');
}
$(section).find('ul').slideDown('fast');
$(section).find('.operation > .content').slideDown('fast');
}); });
{% if enableSandbox %} {% if enableSandbox %}
var getStoredValue, storeValue, deleteStoredValue;
var apiAuthKeys = ['api_key', 'api_login', 'api_pass', 'api_endpoint'];
if ('localStorage' in window) {
var buildKey = function (key) {
return 'nelmio_' + key;
}
getStoredValue = function (key) {
return localStorage.getItem(buildKey(key));
}
storeValue = function (key, value) {
localStorage.setItem(buildKey(key), value);
}
deleteStoredValue = function (key) {
localStorage.removeItem(buildKey(key));
}
} else {
getStoredValue = storeValue = deleteStoredValue = function (){};
}
var loadStoredAuthParams = function() {
$.each(apiAuthKeys, function(_, value) {
var elm = $('#' + value);
if (elm.length) {
elm.val(getStoredValue(value));
}
});
}
var setParameterType = function ($context,setType) {
// no 2nd argument, use default from parameters
if (typeof setType == "undefined") {
setType = $context.parent().attr("data-dataType");
$context.val(setType);
}
$context.parent().find('.value').remove();
var placeholder = "";
if ($context.parent().attr("data-dataType") != "" && typeof $context.parent().attr("data-dataType") != "undefined") {
placeholder += "[" + $context.parent().attr("data-dataType") + "] ";
}
if ($context.parent().attr("data-format") != "" && typeof $context.parent().attr("data-format") != "undefined") {
placeholder += $context.parent().attr("data-format");
}
if ($context.parent().attr("data-description") != "" && typeof $context.parent().attr("data-description") != "undefined") {
placeholder += $context.parent().attr("data-description");
} else {
placeholder += "Value";
}
switch(setType) {
case "boolean":
$('<select class="value"><option value=""></option><option value="1">True</option><option value="0">False</option></select>').insertAfter($context);
break;
case "file":
$('<input type="file" class="value" placeholder="'+ placeholder +'">').insertAfter($context);
break;
case "textarea":
$('<textarea class="value" placeholder="'+ placeholder +'" />').insertAfter($context);
break;
default:
$('<input type="text" class="value" placeholder="'+ placeholder +'">').insertAfter($context);
}
};
var toggleButtonText = function ($btn) { var toggleButtonText = function ($btn) {
if ($btn.text() === 'Default') { if ($btn.text() === 'Default') {
$btn.text('Raw'); $btn.text('Raw');
@ -246,7 +78,7 @@
$btn = $container.parents('.pane').find('.to-prettify'); $btn = $container.parents('.pane').find('.to-prettify');
$container.removeClass('prettyprinted'); $container.removeClass('prettyprinted');
$container.html(attachCollapseMarker(prettifyResponse(rawData))); $container.html(prettifyResponse(rawData));
prettyPrint && prettyPrint(); prettyPrint && prettyPrint();
$btn.removeClass('to-prettify'); $btn.removeClass('to-prettify');
@ -255,50 +87,6 @@
toggleButtonText($btn); toggleButtonText($btn);
}; };
var unflattenDict = function (body) {
var found = true;
while(found) {
found = false;
for (var key in body) {
var okey;
var value = body[key];
var dictMatch = key.match(/^(.+)\[([^\]]+)\]$/);
if(dictMatch) {
found = true;
okey = dictMatch[1];
var subkey = dictMatch[2];
body[okey] = body[okey] || {};
body[okey][subkey] = value;
delete body[key];
} else {
body[key] = value;
}
}
}
return body;
};
$('#save_api_auth').click(function(event) {
$.each(apiAuthKeys, function(_, value) {
var elm = $('#' + value);
if (elm.length) {
storeValue(value, elm.val());
}
});
});
$('#clear_api_auth').click(function(event) {
$.each(apiAuthKeys, function(_, value) {
deleteStoredValue(value);
var elm = $('#' + value);
if (elm.length) {
elm.val('');
}
});
});
$('.tabs li').click(function() { $('.tabs li').click(function() {
var contentGroup = $(this).parents('.content'); var contentGroup = $(this).parents('.content');
@ -309,22 +97,6 @@
$(this).addClass('selected'); $(this).addClass('selected');
}); });
var getJsonCollapseHtml = function(sectionOpenCharacter) {
var $toggler = $('<span>').addClass('json-collapse-section').
attr('data-section-open-character', sectionOpenCharacter).
append($('<span>').addClass('json-collapse-marker')
.html('&#9663;')
).append(sectionOpenCharacter);
return $('<div>').append($toggler).html();
};
var attachCollapseMarker = function (prettifiedJsonString) {
prettifiedJsonString = prettifiedJsonString.replace(/(\{|\[)\n/g, function(match, sectionOpenCharacter) {
return getJsonCollapseHtml(sectionOpenCharacter) + '<span class="json-collapse-content">\n';
});
return prettifiedJsonString.replace(/([^\[][\}\]],?)\n/g, '$1</span>\n');
};
var prettifyResponse = function(text) { var prettifyResponse = function(text) {
try { try {
var data = typeof text === 'string' ? JSON.parse(text) : text; var data = typeof text === 'string' ? JSON.parse(text) : text;
@ -336,34 +108,8 @@
return $('<div>').text(text).html(); return $('<div>').text(text).html();
}; };
var displayFinalUrl = function(xhr, method, url, data, container) { var displayFinalUrl = function(xhr, method, url, container) {
container.text(method + ' ' + getFinalUrl(method, url, data)); container.html(method + ' ' + url);
};
var displayRequestBody = function(method, data, container, header) {
if ('GET' != method && !jQuery.isEmptyObject(data) && data !== "" && data !== undefined) {
if (jQuery.type(data) !== 'string') {
data = decodeURIComponent(jQuery.param(data));
}
container.text(data);
container.show();
header.show();
} else {
container.hide();
header.hide();
}
};
var displayProfilerUrl = function(xhr, link, container) {
var profilerUrl = xhr.getResponseHeader('X-Debug-Token-Link');
if (profilerUrl) {
link.attr('href', profilerUrl);
container.show();
} else {
link.attr('href', '');
container.hide();
}
}; };
var displayResponseData = function(xhr, container) { var displayResponseData = function(xhr, container) {
@ -384,131 +130,37 @@
container.text(text); container.text(text);
}; };
var displayCurl = function(method, url, headers, data, result_container) { var displayResponse = function(xhr, method, url, result_container) {
var escapeShell = function(param) { displayFinalUrl(xhr, method, url, $('.url', result_container));
param = "" + param;
return '"' + param.replace(/(["\s'$`\\])/g,'\\$1') + '"';
};
url = getFinalUrl(method, url, data);
var command = "curl";
command += " -X " + escapeShell(method);
if (method != "GET" && !jQuery.isEmptyObject(data) && data !== "" && data !== undefined) {
if (jQuery.type(data) !== 'string') {
data = decodeURIComponent(jQuery.param(data));
}
command += " -d " + escapeShell(data);
}
for (headerKey in headers) {
if (headers.hasOwnProperty(headerKey)) {
command += " -H " + escapeShell(headerKey + ': ' + headers[headerKey]);
}
}
command += " " + url;
result_container.text(command);
};
var getFinalUrl = function(method, url, data) {
if ('GET' == method && !jQuery.isEmptyObject(data)) {
var separator = url.indexOf('?') >= 0 ? '&' : '?';
url = url + separator + decodeURIComponent(jQuery.param(data));
}
return url;
};
var displayResponse = function(xhr, method, url, headers, data, result_container) {
displayFinalUrl(xhr, method, url, data, $('.url', result_container));
displayRequestBody(method, data, $('.request-body', result_container), $('.request-body-header', result_container));
displayProfilerUrl(xhr, $('.profiler-link', result_container), $('.profiler', result_container));
displayResponseData(xhr, $('.response', result_container)); displayResponseData(xhr, $('.response', result_container));
displayResponseHeaders(xhr, $('.headers', result_container)); displayResponseHeaders(xhr, $('.headers', result_container));
displayCurl(method, url, headers, data, $('.curl-command', result_container));
result_container.show(); result_container.show();
}; };
$('.pane.sandbox form').submit(function() { $('.pane.sandbox form').submit(function() {
var url = $(this).attr('action'), var url = $(this).attr('action'),
method = $('[name="header_method"]', this).val(), method = $(this).attr('method'),
self = this, self = this,
params = {}, params = {},
filters = {},
formData = new FormData(),
doubledParams = {},
doubledFilters = {},
headers = {}, headers = {},
content = $(this).find('textarea.content').val(), content = $(this).find('textarea.content').val(),
result_container = $('.result', $(this).parent()); result_container = $('.result', $(this).parent());
if (method === 'ANY') { if (method === 'ANY') {
method = 'POST'; method = 'POST';
} else if (method.indexOf('|') !== -1) {
method = method.split('|').sort().pop();
} }
// set requestFormat var requestFormat = $('#request_format').val();
var requestFormatMethod = '{{ requestFormatMethod }}'; var requestFormatMethod = '{{ requestFormatMethod }}';
if (requestFormatMethod == 'format_param') { if (requestFormatMethod == 'format_param') {
params['_format'] = $('#request_format option:selected').text(); params['_format'] = requestFormat;
formData.append('_format',$('#request_format option:selected').text());
} else if (requestFormatMethod == 'accept_header') { } else if (requestFormatMethod == 'accept_header') {
headers['Accept'] = $('#request_format').val(); headers['Accept'] = 'application/' + requestFormat;
} }
// set default bodyFormat
var bodyFormat = $('#body_format').val() || '{{ defaultBodyFormat }}';
if(!('Content-type' in headers)) {
if (bodyFormat == 'form') {
headers['Content-type'] = 'application/x-www-form-urlencoded';
} else {
headers['Content-type'] = 'application/json';
}
}
var hasFileTypes = false;
$('.parameters .tuple_type', $(this)).each(function() {
if ($(this).val() == 'file') {
hasFileTypes = true;
}
});
if (hasFileTypes && method != 'POST') {
alert("Sorry, you can only submit files via POST.");
return false;
}
if (hasFileTypes && bodyFormat != 'form') {
alert("Body Format must be set to 'Form Data' when utilizing file upload type parameters.\nYour current bodyFormat is '" + bodyFormat + "'. Change your BodyFormat or do not use file type\nparameters.");
return false;
}
if (hasFileTypes) {
// retrieve all the parameters to send for file upload
$('.parameters .tuple', $(this)).each(function() {
var key, value;
key = $('.key', $(this)).val();
if ($('.value', $(this)).attr('type') === 'file' ) {
value = $('.value', $(this)).prop('files')[0];
if(!value) {
value = new File([], '');
}
} else {
value = $('.value', $(this)).val();
}
if (value) {
formData.append(key,value);
}
});
}
// retrieve all the parameters to send // retrieve all the parameters to send
$('.parameters .tuple', $(this)).each(function() { $('.parameters .tuple', $(this)).each(function() {
var key, value; var key, value;
@ -517,43 +169,10 @@
value = $('.value', $(this)).val(); value = $('.value', $(this)).val();
if (value) { if (value) {
// convert boolean values to boolean params[key] = value;
if ('json' === bodyFormat && 'boolean' === $('.tuple_type', $(this)).val()) {
value = '1' === value;
}
// temporary save all additional/doubled parameters
if (key in params) {
doubledParams[key] = value;
} else {
params[key] = value;
}
} }
}); });
// retrieve all the filters to send
$('.parameters .tuple.filter', $(this)).each(function() {
var key, value;
key = $('.key', $(this)).val();
value = $('.value', $(this)).val();
if (value) {
// temporary save all additional/doubled parameters
if (key in filters) {
doubledFilters[key] = value;
} else {
filters[key] = value;
}
}
});
// retrieve the additional headers to send // retrieve the additional headers to send
$('.headers .tuple', $(this)).each(function() { $('.headers .tuple', $(this)).each(function() {
var key, value; var key, value;
@ -564,7 +183,6 @@
if (value) { if (value) {
headers[key] = value; headers[key] = value;
} }
}); });
// fix parameters in URL // fix parameters in URL
@ -575,139 +193,61 @@
} }
}; };
// merge additional params back to real params object
if (!$.isEmptyObject(doubledParams)) {
$.extend(params, doubledParams);
}
// disable all the fiels and buttons // disable all the fiels and buttons
$('input, button', $(this)).attr('disabled', 'disabled'); $('input, button', $(this)).attr('disabled', 'disabled');
// append the query authentication // append the query authentication
var api_key_val = $('#api_key').val(); if (authentication_delivery == 'query') {
if (authentication_delivery == 'query' && api_key_val.length>0) {
url += url.indexOf('?') > 0 ? '&' : '?'; url += url.indexOf('?') > 0 ? '&' : '?';
url += api_key_parameter + '=' + api_key_val; url += api_key_parameter + '=' + $('#api_key').val();
} }
// prepare the api enpoint // prepare the api enpoint
{% if endpoint == '' and app.request is not null and app.request.host -%} {% if endpoint == '' and app.request is defined and app.request.host -%}
var endpoint = '{{ app.request.getBaseUrl() }}'; var endpoint = '{{ app.request.getBaseUrl() }}';
{% else -%} {% else -%}
var endpoint = '{{ endpoint }}'; var endpoint = '{{ endpoint }}';
{% endif -%} {% endif -%}
{% if authentication and authentication.custom_endpoint %} if ($('#api_endpoint') && $('#api_endpoint').val() != null) {
if ($('#api_endpoint') && typeof($('#api_endpoint').val()) != 'undefined') {
endpoint = $('#api_endpoint').val(); endpoint = $('#api_endpoint').val();
} }
{% endif %}
//add filters as GET params and remove them from params // and trigger the API call
if(method != 'GET'){ $.ajax({
for (var filterKey in $.extend({}, filters)){ url: endpoint + url,
url += url.indexOf('?') > 0 ? '&' : '?';
url += filterKey + '=' + filters[filterKey];
if (params.hasOwnProperty(filterKey)){
delete(params[filterKey]);
}
}
}
// prepare final parameters
var body = {};
if(bodyFormat == 'json' && method != 'GET') {
body = unflattenDict(params);
body = JSON.stringify(body);
} else {
body = params;
}
var data = content.length ? content : body;
var ajaxOptions = {
url: (url.indexOf('http')!=0?endpoint:'') + url,
xhrFields: { withCredentials: true },
type: method, type: method,
data: data, data: content.length ? content : params,
headers: headers, headers: headers,
crossDomain: true, crossDomain: true,
beforeSend: function (xhr) { beforeSend: function (xhr) {
if (authentication_delivery) { if (authentication_delivery == 'http_basic') {
var value; xhr.setRequestHeader('Authorization', 'Basic ' + btoa($('#api_key').val() + ':' + $('#api_pass').val()));
if ('http' == authentication_delivery) {
if ('basic' == authentication_type) {
value = 'Basic ' + btoa($('#api_login').val() + ':' + $('#api_pass').val());
} else if ('bearer' == authentication_type) {
value = 'Bearer ' + $('#api_key').val();
}
} else if ('header' == authentication_delivery) {
value = $('#api_key').val();
}
xhr.setRequestHeader(api_key_parameter, value);
} }
}, },
complete: function(xhr) { complete: function(xhr) {
displayResponse(xhr, method, url, headers, data, result_container); displayResponse(xhr, method, url, result_container);
// and enable them back // and enable them back
$('input:not(.content-type), button', $(self)).removeAttr('disabled'); $('input:not(.content-type), button', $(self)).removeAttr('disabled');
} }
}; });
// overrides body format to send data properly
if (hasFileTypes) {
ajaxOptions.data = formData;
ajaxOptions.processData = false;
ajaxOptions.contentType = false;
delete(headers['Content-type']);
}
// and trigger the API call
$.ajax(ajaxOptions);
return false; return false;
}); });
$('.operations').on('click', '.operation > .heading', function(e) { $('.pane.sandbox').delegate('.to-raw', 'click', function(e) {
if (history.pushState) {
history.pushState(null, null, $(this).data('href'));
e.preventDefault();
}
});
$(document).on('click', '.json-collapse-section', function() {
var openChar = $(this).data('section-open-character'),
closingChar = (openChar == '{' ? '}' : ']');
if ($(this).next('.json-collapse-content').is(':visible')) {
$(this).html('&oplus;' + openChar + '...' + closingChar);
} else {
$(this).html('&#9663;' + $(this).data('section-open-character'));
}
$(this).next('.json-collapse-content').toggle();
});
$(document).on('copy', '.prettyprinted', function () {
var $toggleMarkers = $(this).find('.json-collapse-marker');
$toggleMarkers.hide();
setTimeout(function () {
$toggleMarkers.show();
}, 100);
});
$('.pane.sandbox').on('click', '.to-raw', function(e) {
renderRawBody($(this).parents('.pane').find('.response')); renderRawBody($(this).parents('.pane').find('.response'));
e.preventDefault(); e.preventDefault();
}); });
$('.pane.sandbox').on('click', '.to-prettify', function(e) { $('.pane.sandbox').delegate('.to-prettify', 'click', function(e) {
renderPrettifiedBody($(this).parents('.pane').find('.response')); renderPrettifiedBody($(this).parents('.pane').find('.response'));
e.preventDefault(); e.preventDefault();
}); });
$('.pane.sandbox').on('click', '.to-expand, .to-shrink', function(e) { $('.pane.sandbox').delegate('.to-expand, .to-shrink', 'click', function(e) {
var $headers = $(this).parents('.result').find('.headers'); var $headers = $(this).parents('.result').find('.headers');
var $label = $(this).parents('.result').find('a.to-expand'); var $label = $(this).parents('.result').find('a.to-expand');
@ -724,30 +264,8 @@
e.preventDefault(); e.preventDefault();
}); });
$('.pane.sandbox').on('click', '.add', function() {
// sets the correct parameter type on load var html = $(this).parents('.pane').find('.tuple_template').html();
$('.pane.sandbox .tuple_type').each(function() {
setParameterType($(this));
});
// handles parameter type change
$('.pane.sandbox').on('change', '.tuple_type', function() {
setParameterType($(this),$(this).val());
});
$('.pane.sandbox').on('click', '.add_parameter', function() {
var html = $(this).parents('.pane').find('.parameters_tuple_template').html();
$(this).before(html);
return false;
});
$('.pane.sandbox').on('click', '.add_header', function() {
var html = $(this).parents('.pane').find('.headers_tuple_template').html();
$(this).before(html); $(this).before(html);
@ -788,16 +306,14 @@
}); });
{% if authentication and authentication.delivery == 'http' %} {% if authentication and authentication.delivery == 'http_basic' %}
var authentication_delivery = '{{ authentication.delivery }}'; var authentication_delivery = '{{ authentication.delivery }}';
var api_key_parameter = '{{ authentication.name }}';
var authentication_type = '{{ authentication.type }}';
{% elseif authentication and authentication.delivery == 'query' %} {% elseif authentication and authentication.delivery == 'query' %}
var authentication_delivery = '{{ authentication.delivery }}'; var authentication_delivery = '{{ authentication.delivery }}';
var api_key_parameter = '{{ authentication.name }}'; var api_key_parameter = '{{ authentication.name }}';
var search = window.location.search; var search = window.location.search;
var api_key_start = search.indexOf(api_key_parameter) + api_key_parameter.length + 1; var api_key_start = search.indexOf(api_key_parameter) + api_key_parameter.length + 1;
if (api_key_start > 0 ) { if (api_key_start > 0 ) {
var api_key_end = search.indexOf('&', api_key_start); var api_key_end = search.indexOf('&', api_key_start);
@ -807,9 +323,6 @@
$('#api_key').val(api_key); $('#api_key').val(api_key);
} }
{% elseif authentication and authentication.delivery == 'header' %}
var authentication_delivery = '{{ authentication.delivery }}';
var api_key_parameter = '{{ authentication.name }}';
{% else %} {% else %}
var authentication_delivery = false; var authentication_delivery = false;
{% endif %} {% endif %}

View file

@ -1,40 +1,42 @@
<li class="{{ data.method|lower }} operation" id="{{ data.id }}"> <li class="{{ data.method|lower }} operation">
<div class="heading toggler{% if data.deprecated %} deprecated{% endif %}" data-href="#{{ data.id }}"> <div class="heading toggler{% if data.deprecated %} deprecated{% endif %}">
<h3> <h3>
<span class="http_method"> <span class="http_method">
<i>{{ data.method|upper }}</i> <a>{{ data.method|upper }}</a>
</span> </span>
{% if data.deprecated %} {% if data.deprecated %}
<span class="deprecated"> <span class="deprecated">
<i>DEPRECATED</i> <a>DEPRECATED</a>
</span> </span>
{% endif %} {% endif %}
<span class="path"> {% if data.https %}
{% if data.host is defined -%} <img src="{{ asset('bundles/nelmioapidoc/image/lock.png') }}" class="icon" alt="HTTPS" />
{{ data.https ? 'https://' : 'http://' -}} {% endif %}
{{ data.host -}} {% if data.authentication %}
{% endif -%} <img src="{{ asset('bundles/nelmioapidoc/image/keys.png') }}" class="icon" alt="Needs authentication" />
{{ data.uri }} {% endif %}
</span>
{% if data.tags is defined %} <span class="path">
{% for tag, color_code in data.tags %} {% if data.host is defined -%}
<span class="tag" {% if color_code is defined and color_code is not empty %}style="background-color:{{ color_code }};"{% endif %}>{{ tag }}</span> {{ data.https ? 'https://' : 'http://' -}}
{% endfor %} {{ data.host -}}
{% endif %} {% endif -%}
</h3> {{ data.uri }}
<ul class="options"> </span>
{% if data.description is defined %} </h3>
<li>{{ data.description }}</li> <ul class="options">
{% endif %} {% if data.description is defined %}
</ul> <li>{{ data.description }}</li>
{% endif %}
</ul>
</div> </div>
<div class="content" style="display: {% if displayContent is defined and displayContent == true %}display{% else %}none{% endif %};"> <div class="content" style="display: {% if displayContent is defined and displayContent == true %}display{% else %}none{% endif %};">
<ul class="tabs"> <ul class="tabs">
<li class="selected" data-pane="content">Documentation</li>
{% if enableSandbox %} {% if enableSandbox %}
<li class="selected" data-pane="content">Documentation</li>
<li data-pane="sandbox">Sandbox</li> <li data-pane="sandbox">Sandbox</li>
{% endif %} {% endif %}
</ul> </ul>
@ -46,12 +48,7 @@
<div>{{ data.documentation|extra_markdown }}</div> <div>{{ data.documentation|extra_markdown }}</div>
{% endif %} {% endif %}
{% if data.link is defined and data.link is not empty %} {% if data.requirements is defined and data.requirements is not empty %}
<h4>Link</h4>
<div><a href="{{ data.link }}" target="_blank">{{ data.link }}</a></div>
{% endif %}
{% if data.requirements is defined and data.requirements is not empty %}
<h4>Requirements</h4> <h4>Requirements</h4>
<table class="fullwidth"> <table class="fullwidth">
<thead> <thead>
@ -66,9 +63,9 @@
{% for name, infos in data.requirements %} {% for name, infos in data.requirements %}
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ infos.requirement is defined ? infos.requirement : ''}}</td> <td>{{ infos.requirement }}</td>
<td>{{ infos.dataType is defined ? infos.dataType : ''}}</td> <td>{{ infos.dataType }}</td>
<td>{{ infos.description is defined ? infos.description : ''}}</td> <td>{{ infos.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -93,7 +90,7 @@
{% for key, value in infos %} {% for key, value in infos %}
<tr> <tr>
<td>{{ key|title }}</td> <td>{{ key|title }}</td>
<td>{{ value|json_encode(constant('JSON_UNESCAPED_UNICODE'))|replace({'\\\\': '\\'})|trim('"') }}</td> <td>{{ value|json_encode|replace({'\\\\': '\\'})|trim('"') }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
@ -112,7 +109,6 @@
<th>Parameter</th> <th>Parameter</th>
<th>Type</th> <th>Type</th>
<th>Required?</th> <th>Required?</th>
<th>Format</th>
<th>Description</th> <th>Description</th>
</tr> </tr>
</thead> </thead>
@ -121,10 +117,9 @@
{% if not infos.readonly %} {% if not infos.readonly %}
<tr> <tr>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ infos.dataType is defined ? infos.dataType : '' }}</td> <td>{{ infos.dataType }}</td>
<td>{{ infos.required ? 'true' : 'false' }}</td> <td>{{ infos.required ? 'true' : 'false' }}</td>
<td class="format">{{ infos.format }}</td> <td>{{ infos.description }}</td>
<td>{{ infos.description is defined ? infos.description|trans : '' }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -132,62 +127,25 @@
</table> </table>
{% endif %} {% endif %}
{% if data.response is defined and data.response is not empty %}
{% if data.headers is defined and data.headers is not empty %}
<h4>Headers</h4>
<table class="fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Required?</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for name, infos in data.headers %}
<tr>
<td>{{ name }}</td>
<td>{{ infos.required is defined and infos.required == 'true' ? 'true' : 'false'}}</td>
<td>{{ infos.description is defined ? infos.description|trans : ''}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if data.parsedResponseMap is defined and data.parsedResponseMap is not empty %}
<h4>Return</h4> <h4>Return</h4>
<table class='fullwidth'> <table class='fullwidth'>
<thead> <thead>
<tr> <tr>
<th>Parameter</th> <th>Parameter</th>
<th>Type</th> <th>Type</th>
<th>Versions</th> <th>Description</th>
<th>Description</th> </tr>
</tr>
</thead> </thead>
{% for status_code, response in data.parsedResponseMap %}
<tbody> <tbody>
<tr> {% for name, infos in data.response %}
<td> <tr>
<h4> <td>{{ name }}</td>
{{ status_code }} <td>{{ infos.dataType }}</td>
{% if data.statusCodes is defined and data.statusCodes[status_code] is defined %} <td>{{ infos.description }}</td>
- {{ data.statusCodes[status_code]|join(', ') }} </tr>
{% endif %} {% endfor %}
</h4>
</td>
</tr>
{% for name, infos in response.model %}
<tr>
<td>{{ name }}</td>
<td>{{ infos.dataType }}</td>
<td>{% include '@NelmioApiDoc/Components/version.html.twig' with {'sinceVersion': infos.sinceVersion, 'untilVersion': infos.untilVersion} only %}</td>
<td>{{ infos.description }}</td>
</tr>
{% endfor %}
</tbody> </tbody>
{% endfor %}
</table> </table>
{% endif %} {% endif %}
@ -203,7 +161,7 @@
<tbody> <tbody>
{% for status_code, descriptions in data.statusCodes %} {% for status_code, descriptions in data.statusCodes %}
<tr> <tr>
<td><a href="http://en.wikipedia.org/wiki/HTTP_{{ status_code }}" target="_blank">{{ status_code }}</a></td> <td><a href="http://en.wikipedia.org/wiki/HTTP_{{ status_code }}" target="_blank">{{ status_code }}<a/></td>
<td> <td>
<ul> <ul>
{% for description in descriptions %} {% for description in descriptions %}
@ -226,158 +184,101 @@
{% if enableSandbox %} {% if enableSandbox %}
<div class="pane sandbox"> <div class="pane sandbox">
{% if app.request is not null and data.https and app.request.secure != data.https %} <form method="{{ data.method|upper }}" action="{{ data.uri }}">
Please reload the documentation using the scheme HTTP if you want to use the sandbox. <fieldset class="parameters">
{% else %} <legend>Input</legend>
<form method="" action="{% if data.host is defined %}http://{{ data.host }}{% endif %}{{ data.uri }}"> {% if data.requirements is defined %}
<fieldset class="parameters"> <h4>Requirements</h4>
<legend>Input</legend> {% for name, infos in data.requirements %}
{% if data.requirements is defined %}
<h4>Requirements</h4>
{% for name, infos in data.requirements %}
<p class="tuple">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.description is defined %}{{ infos.description }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
{% if data.filters is defined %}
<h4>Filters</h4>
{% for name, infos in data.filters %}
<p class="tuple filter">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.description is defined %}{{ infos.description }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
{% if data.parameters is defined %}
<h4>Parameters</h4>
{% for name, infos in data.parameters %}
{% if not infos.readonly %}
<p class="tuple" data-dataType="{% if infos.dataType %}{{ infos.dataType }}{% endif %}" data-format="{% if infos.format %}{{ infos.format }} {% endif %}" data-description="{% if infos.description %}{{ infos.description|trans }}{% endif %}">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<select class="tuple_type">
<option value="">Type</option>
<option value="string">String</option>
<option value="boolean">Boolean</option>
<option value="file">File</option>
<option value="textarea">Textarea</option>
</select>
<input type="text" class="value" placeholder="{% if infos.dataType %}[{{ infos.dataType }}] {% endif %}{% if infos.format %}{{ infos.format }}{% endif %}{% if infos.description %}{{ infos.description|trans }}{% else %}Value{% endif %}" {% if infos.default is defined %} value="{{ infos.default }}" {% endif %}/> <span class="remove">-</span>
</p>
{% endif %}
{% endfor %}
<button type="button" class="add_parameter">New parameter</button>
{% endif %}
</fieldset>
<fieldset class="headers">
{% set methods = data.method|upper|split('|') %}
{% if methods|length > 1 %}
<legend>Method</legend>
<select name="header_method">
{% for method in methods %}
<option value="{{ method }}">{{ method }}</option>
{% endfor %}
</select>
{% else %}
<input type="hidden" name="header_method" value="{{ methods|join }}" />
{% endif %}
<legend>Headers</legend>
{% if acceptType %}
<p class="tuple"> <p class="tuple">
<input type="text" class="key" value="Accept" /> <input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span> <span>=</span>
<input type="text" class="value" value="{{ acceptType }}" /> <span class="remove">-</span> <input type="text" class="value" placeholder="{% if infos.description is defined %}{{ infos.description }}{% else %}Value{% endif %}" /> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
{% if data.filters is defined %}
<h4>Filters</h4>
{% for name, infos in data.filters %}
<p class="tuple">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.description is defined %}{{ infos.description }}{% else %}Value{% endif %}" /> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
{% if data.parameters is defined %}
<h4>Parameters</h4>
{% for name, infos in data.parameters %}
{% if not infos.readonly %}
<p class="tuple">
<input type="text" class="key" value="{{ name }}" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="{% if infos.dataType %}[{{ infos.dataType }}] {% endif %}{% if infos.description %}{{ infos.description }}{% else %}Value{% endif %}" /> <span class="remove">-</span>
</p> </p>
{% endif %} {% endif %}
{% endfor %}
<button class="add">New parameter</button>
{% endif %}
{% if data.headers is defined %} </fieldset>
{% for name, infos in data.headers %} <fieldset class="headers">
<p class="tuple"> <legend>Headers</legend>
<input type="text" class="key" value="{{ name }}" />
<span>=</span>
<input type="text" class="value" value="{% if infos.default is defined %}{{ infos.default }}{% endif %}" placeholder="Value" /> <span class="remove">-</span>
</p>
{% endfor %}
{% endif %}
{% if acceptType %}
<p class="tuple"> <p class="tuple">
<input type="text" class="key" placeholder="Key" /> <input type="text" class="key" value="Accept" />
<span>=</span> <span>=</span>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span> <input type="text" class="value" value="{{ acceptType }}" /> <span class="remove">-</span>
</p> </p>
{% endif %}
<button type="button" class="add_header">New header</button> <p class="tuple">
</fieldset> <input type="text" class="key" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
<fieldset class="request-content"> <button class="add">New header</button>
<legend>Content</legend> </fieldset>
<textarea class="content" placeholder="Content set here will override the parameters that do not match the url"></textarea> <fieldset class="request-content">
<legend>Content</legend>
<p class="tuple"> <textarea class="content" placeholder="Content set here will override the parameters that do not match the url"></textarea>
<input type="text" class="key content-type" value="Content-Type" disabled="disabled" />
<span>=</span>
<input type="text" class="value" placeholder="Value" />
<button type="button" class="set-content-type">Set header</button> <small>Replaces header if set</small>
</p>
</fieldset>
<div class="buttons"> <p class="tuple">
<input type="submit" value="Try!" /> <input type="text" class="key content-type" value="Content-Type" disabled="disabled" />
</div> <span>=</span>
</form> <input type="text" class="value" placeholder="Value" />
<button class="set-content-type">Set header</button> <small>Replaces header if set</small>
</p>
</fieldset>
<script type="text/x-tmpl" class="parameters_tuple_template"> <div class="buttons">
<p class="tuple"> <input type="submit" value="Try!" />
<input type="text" class="key" placeholder="Key" />
<span>=</span>
<select class="tuple_type">
<option value="">Type</option>
<option value="string">String</option>
<option value="boolean">Boolean</option>
<option value="file">File</option>
<option value="textarea">Textarea</option>
</select>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
</script>
<script type="text/x-tmpl" class="headers_tuple_template">
<p class="tuple">
<input type="text" class="key" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
</script>
<div class="result">
<h4>Request URL</h4>
<pre class="url"></pre>
<h4 class="request-body-header">Request body</h4>
<pre class="request-body"></pre>
<h4>Response Headers&nbsp;<small>[<a href="" class="to-expand">Expand</a>]</small>&nbsp;<small class="profiler">[<a href="" class="profiler-link" target="_blank">Profiler</a>]</small></h4>
<pre class="headers to-expand"></pre>
<h4>Response Body&nbsp;<small>[<a href="" class="to-raw">Raw</a>]</small></h4>
<pre class="response prettyprint"></pre>
<h4>Curl Command Line</h4>
<pre class="curl-command"></pre>
</div> </div>
{% endif %} </form>
<script type="text/x-tmpl" class="tuple_template">
<p class="tuple">
<input type="text" class="key" placeholder="Key" />
<span>=</span>
<input type="text" class="value" placeholder="Value" /> <span class="remove">-</span>
</p>
</script>
<div class="result">
<h4>Request URL</h4>
<pre class="url"></pre>
<h4>Response Headers&nbsp;<small>[<a href="" class="to-expand">Expand</a>]</small></h4>
<pre class="headers to-expand"></pre>
<h4>Response Body&nbsp;<small>[<a href="" class="to-raw">Raw</a>]</small></h4>
<pre class="response prettyprint"></pre>
</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,11 +1,11 @@
{% extends "@NelmioApiDoc/layout.html.twig" %} {% extends "NelmioApiDocBundle::layout.html.twig" %}
{% block content %} {% block content %}
<li class="resource"> <li class="resource">
<ul class="endpoints"> <ul class="endpoints">
<li class="endpoint"> <li class="endpoint">
<ul class="operations"> <ul class="operations">
{% include '@NelmioApiDoc/method.html.twig' %} {% include 'NelmioApiDocBundle::method.html.twig' %}
</ul> </ul>
</li> </li>
</ul> </ul>

View file

@ -1,26 +1,12 @@
{% extends "@NelmioApiDoc/layout.html.twig" %} {% extends "NelmioApiDocBundle::layout.html.twig" %}
{% block content %} {% block content %}
<div id="summary">
<ul>
{% for section, sections in resources %}
<li><a href="#section-{{ section }}">{{ section }}</a></li>
{% endfor %}
</ul>
</div>
{% for section, sections in resources %} {% for section, sections in resources %}
{% if section != '_others' %} {% if section != '_others' %}
<li class="section{{ defaultSectionsOpened? ' active':'' }}"> <div id="section">
<div class="actions"> <h1>{{ section }}</h1>
<a class="action-show-hide">Show/hide</a>
<a class="action-list">List Operations</a>
<a class="action-expand">Expand Operations</a>
</div>
<h1>{{ section }}</h1>
<ul class="section-list" {% if not defaultSectionsOpened %}style="display: none"{% endif %}>
{% endif %} {% endif %}
{% for resource, methods in sections %} {% for resource, methods in sections %}
<a id="section-{{ section }}"></a>
<li class="resource"> <li class="resource">
<div class="heading"> <div class="heading">
{% if section == '_others' and resource != 'others' %} {% if section == '_others' and resource != 'others' %}
@ -33,7 +19,7 @@
<li class="endpoint"> <li class="endpoint">
<ul class="operations"> <ul class="operations">
{% for data in methods %} {% for data in methods %}
{% include '@NelmioApiDoc/method.html.twig' %} {% include 'NelmioApiDocBundle::method.html.twig' %}
{% endfor %} {% endfor %}
</ul> </ul>
</li> </li>
@ -41,8 +27,7 @@
</li> </li>
{% endfor %} {% endfor %}
{% if section != '_others' %} {% if section != '_others' %}
</ul> </div>
</li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endblock content %} {% endblock content %}

View file

@ -1,236 +0,0 @@
<?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\Swagger;
use Nelmio\ApiDocBundle\DataTypes;
/**
* Class ModelRegistry
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class ModelRegistry
{
/**
* @var array
*/
protected $namingStrategies = [
'dot_notation' => 'nameDotNotation',
'last_segment_only' => 'nameLastSegmentOnly',
];
/**
* @var array
*/
protected $models = [];
protected $classes = [];
/**
* @var callable
*/
protected $namingStategy;
protected $typeMap = [
DataTypes::INTEGER => 'integer',
DataTypes::FLOAT => 'number',
DataTypes::STRING => 'string',
DataTypes::BOOLEAN => 'boolean',
DataTypes::FILE => 'string',
DataTypes::DATE => 'string',
DataTypes::DATETIME => 'string',
];
protected $formatMap = [
DataTypes::INTEGER => 'int32',
DataTypes::FLOAT => 'float',
DataTypes::FILE => 'byte',
DataTypes::DATE => 'date',
DataTypes::DATETIME => 'date-time',
];
public function __construct($namingStrategy)
{
if (!isset($this->namingStrategies[$namingStrategy])) {
throw new \InvalidArgumentException(sprintf(
'Invalid naming strategy. Choose from: %s',
json_encode(array_keys($this->namingStrategies))
));
}
$this->namingStategy = [$this, $this->namingStrategies[$namingStrategy]];
}
public function register($className, ?array $parameters = null, $description = '')
{
if (!isset($this->classes[$className])) {
$this->classes[$className] = [];
}
$id = call_user_func_array($this->namingStategy, [$className]);
if (isset($this->models[$id])) {
return $id;
}
$this->classes[$className][] = $id;
$model = [
'id' => $id,
'description' => $description,
];
if (is_array($parameters)) {
$required = [];
$properties = [];
foreach ($parameters as $name => $prop) {
$subParam = [];
if (DataTypes::MODEL === $prop['actualType']) {
$subParam['$ref'] = $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
} else {
$type = null;
$format = null;
$items = null;
$enum = null;
$ref = null;
if (isset($this->typeMap[$prop['actualType']])) {
$type = $this->typeMap[$prop['actualType']];
} else {
switch ($prop['actualType']) {
case DataTypes::ENUM:
$type = 'string';
if (isset($prop['format'])) {
$enum = array_keys(json_decode($prop['format'], true));
}
break;
case DataTypes::COLLECTION:
$type = 'array';
if (null === $prop['subType']) {
$items = [
'type' => 'string',
];
} elseif (isset($this->typeMap[$prop['subType']])) {
$items = [
'type' => $this->typeMap[$prop['subType']],
];
} elseif (!isset($this->typeMap[$prop['subType']])) {
$items = [
'$ref' => $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
),
];
}
/* @TODO: Handle recursion if subtype is a model. */
break;
case DataTypes::MODEL:
$ref = $this->register(
$prop['subType'],
$prop['children'] ?? null,
$prop['description'] ?: $prop['dataType']
);
$type = $ref;
/* @TODO: Handle recursion. */
break;
}
}
if (isset($this->formatMap[$prop['actualType']])) {
$format = $this->formatMap[$prop['actualType']];
}
$subParam = [
'type' => $type,
'description' => false === empty($prop['description']) ? (string) $prop['description'] : $prop['dataType'],
];
if (null !== $format) {
$subParam['format'] = $format;
}
if (null !== $enum) {
$subParam['enum'] = $enum;
}
if (null !== $ref) {
$subParam['$ref'] = $ref;
}
if (null !== $items) {
$subParam['items'] = $items;
}
if ($prop['required']) {
$required[] = $name;
}
}
$properties[$name] = $subParam;
}
$model['properties'] = $properties;
$model['required'] = $required;
$this->models[$id] = $model;
}
return $id;
}
public function nameDotNotation($className)
{
/*
* Converts \Fully\Qualified\Class\Name to Fully.Qualified.Class.Name
* "[...]" in aliased and non-aliased collections preserved.
*/
$id = preg_replace('#(\\\|[^A-Za-z0-9\[\]])#', '.', $className);
// Replace duplicate dots.
$id = preg_replace('/\.+/', '.', $id);
// Replace trailing dots.
$id = preg_replace('/^\./', '', $id);
return $id;
}
public function nameLastSegmentOnly($className)
{
/*
* Converts \Fully\Qualified\ClassName to ClassName
*/
$segments = explode('\\', $className);
$id = end($segments);
return $id;
}
public function getModels()
{
return $this->models;
}
public function clear(): void
{
$this->models = [];
$this->classes = [];
}
}

View file

@ -11,32 +11,16 @@
namespace Nelmio\ApiDocBundle\Tests\Annotation; namespace Nelmio\ApiDocBundle\Tests\Annotation;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Tests\TestCase; use Nelmio\ApiDocBundle\Tests\TestCase;
use Symfony\Component\Routing\Route;
class ApiDocTest extends TestCase class ApiDocTest extends TestCase
{ {
public function testConstructWithoutData(): void public function testConstructWithoutData()
{ {
$annot = new ApiDoc(); $data = array();
$array = $annot->toArray();
$this->assertTrue(is_array($array)); $annot = new ApiDoc($data);
$this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource());
$this->assertEmpty($annot->getViews());
$this->assertFalse($annot->getDeprecated());
$this->assertFalse(isset($array['description']));
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertNull($annot->getInput());
$this->assertFalse(isset($array['headers']));
}
public function testConstructWithInvalidData(): void
{
$annot = new ApiDoc();
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
@ -44,41 +28,53 @@ class ApiDocTest extends TestCase
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertFalse($annot->getDeprecated()); $this->assertFalse($annot->getDeprecated());
$this->assertFalse(isset($array['description'])); $this->assertFalse(isset($array['description']));
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertNull($annot->getInput()); $this->assertNull($annot->getInput());
$this->assertFalse($array['authentication']);
} }
public function testConstruct(): void public function testConstructWithInvalidData()
{ {
$data = [ $data = array(
'description' => 'Heya', 'unknown' => 'foo',
]; 'array' => array('bar' => 'bar'),
$annot = new ApiDoc(description: $data['description']);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource());
$this->assertFalse($annot->getDeprecated());
$this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertNull($annot->getInput());
}
public function testConstructDefinesAFormType(): void
{
$data = [
'description' => 'Heya',
'input' => 'My\Form\Type',
];
$annot = new ApiDoc(
description: $data['description'],
input: $data['input']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource());
$this->assertFalse($annot->getDeprecated());
$this->assertFalse(isset($array['description']));
$this->assertNull($annot->getInput());
}
public function testConstruct()
{
$data = array(
'description' => 'Heya',
);
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource());
$this->assertFalse($annot->getDeprecated());
$this->assertEquals($data['description'], $array['description']);
$this->assertNull($annot->getInput());
}
public function testConstructDefinesAFormType()
{
$data = array(
'description' => 'Heya',
'input' => 'My\Form\Type',
);
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
@ -86,26 +82,19 @@ class ApiDocTest extends TestCase
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertFalse($annot->getDeprecated()); $this->assertFalse($annot->getDeprecated());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertEquals($data['input'], $annot->getInput()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodIsResource(): void public function testConstructMethodIsResource()
{ {
$data = [ $data = array(
'resource' => true, 'resource' => true,
'description' => 'Heya', 'description' => 'Heya',
'deprecated' => true, 'deprecated' => true,
'input' => 'My\Form\Type', 'input' => 'My\Form\Type',
];
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
input: $data['input']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
@ -113,298 +102,134 @@ class ApiDocTest extends TestCase
$this->assertTrue($annot->isResource()); $this->assertTrue($annot->isResource());
$this->assertTrue($annot->getDeprecated()); $this->assertTrue($annot->getDeprecated());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertEquals($data['input'], $annot->getInput()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodResourceIsFalse(): void public function testConstructMethodResourceIsFalse()
{ {
$data = [ $data = array(
'resource' => false, 'resource' => false,
'description' => 'Heya', 'description' => 'Heya',
'deprecated' => false, 'deprecated' => false,
'input' => 'My\Form\Type', 'input' => 'My\Form\Type',
];
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
input: $data['input']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters'])); $this->assertFalse(isset($array['filters']));
$this->assertFalse($annot->isResource()); $this->assertFalse($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertEquals($data['deprecated'], $array['deprecated']); $this->assertEquals($data['deprecated'], $array['deprecated']);
$this->assertEquals($data['input'], $annot->getInput()); $this->assertEquals($data['input'], $annot->getInput());
} }
public function testConstructMethodHasFilters(): void public function testConstructMethodHasFilters()
{ {
$data = [ $data = array(
'resource' => true, 'resource' => true,
'deprecated' => false, 'deprecated' => false,
'description' => 'Heya', 'description' => 'Heya',
'filters' => [ 'filters' => array(
['name' => 'a-filter'], array('name' => 'a-filter'),
], ),
];
$annot = new ApiDoc(
resource: $data['resource'],
description: $data['description'],
deprecated: $data['deprecated'],
filters: $data['filters']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['filters'])); $this->assertTrue(is_array($array['filters']));
$this->assertCount(1, $array['filters']); $this->assertCount(1, $array['filters']);
$this->assertEquals(['a-filter' => []], $array['filters']); $this->assertEquals(array('a-filter' => array()), $array['filters']);
$this->assertTrue($annot->isResource()); $this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']); $this->assertEquals($data['description'], $array['description']);
$this->assertFalse(isset($array['requirements']));
$this->assertFalse(isset($array['parameters']));
$this->assertEquals($data['deprecated'], $array['deprecated']); $this->assertEquals($data['deprecated'], $array['deprecated']);
$this->assertNull($annot->getInput()); $this->assertNull($annot->getInput());
} }
public function testConstructMethodHasFiltersWithoutName(): void /**
* @expectedException \InvalidArgumentException
*/
public function testConstructMethodHasFiltersWithoutName()
{ {
$this->expectException(\InvalidArgumentException::class); $data = array(
'description' => 'Heya',
$data = [ 'filters' => array(
'description' => 'Heya', array('parameter' => 'foo'),
'filters' => [ ),
['parameter' => 'foo'],
],
];
$annot = new ApiDoc(
description: $data['description'],
filters: $data['filters']
); );
$annot = new ApiDoc($data);
} }
public function testConstructWithStatusCodes(): void public function testConstructNoFiltersIfFormTypeDefined()
{ {
$data = [ $data = array(
'description' => 'Heya', 'resource' => true,
'statusCodes' => [ 'description' => 'Heya',
200 => 'Returned when successful', 'input' => 'My\Form\Type',
403 => 'Returned when the user is not authorized', 'filters' => array(
404 => [ array('name' => 'a-filter'),
'Returned when the user is not found', ),
'Returned when when something else is not found',
],
],
];
$annot = new ApiDoc(
description: $data['description'],
statusCodes: $data['statusCodes']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertFalse(isset($array['filters']));
$this->assertTrue($annot->isResource());
$this->assertEquals($data['description'], $array['description']);
$this->assertEquals($data['input'], $annot->getInput());
}
public function testConstructWithStatusCodes()
{
$data = array(
'description' => 'Heya',
'statusCodes' => array(
200 => "Returned when successful",
403 => "Returned when the user is not authorized",
404 => array(
"Returned when the user is not found",
"Returned when when something else is not found"
)
)
);
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['statusCodes'])); $this->assertTrue(is_array($array['statusCodes']));
foreach ($data['statusCodes'] as $code => $message) { foreach ($data['statusCodes'] as $code => $message) {
$this->assertEquals($array['statusCodes'][$code], !is_array($message) ? [$message] : $message); $this->assertEquals($array['statusCodes'][$code], !is_array($message) ? array($message) : $message);
} }
} }
public function testConstructWithRequirements(): void public function testConstructWithAuthentication()
{ {
$data = [ $data = array(
'requirements' => [ 'authentication' => true
[
'name' => 'fooId',
'requirement' => '\d+',
'dataType' => 'integer',
'description' => 'This requirement might be used withing action method directly from Request object',
],
],
];
$annot = new ApiDoc(
requirements: $data['requirements']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertTrue($array['authentication']);
$this->assertTrue(isset($array['requirements']['fooId']));
$this->assertTrue(isset($array['requirements']['fooId']['dataType']));
} }
public function testConstructWithParameters(): void public function testConstructWithCache()
{ {
$data = [ $data = array(
'parameters' => [ 'cache' => '60'
[
'name' => 'fooId',
'dataType' => 'integer',
'description' => 'Some description',
],
],
];
$annot = new ApiDoc(
parameters: $data['parameters']
); );
$annot = new ApiDoc($data);
$array = $annot->toArray(); $array = $annot->toArray();
$this->assertTrue(is_array($array)); $this->assertEquals($data['cache'], $array['cache']);
$this->assertTrue(isset($array['parameters']['fooId']));
$this->assertTrue(isset($array['parameters']['fooId']['dataType']));
}
public function testConstructWithHeaders(): void
{
$data = [
'headers' => [
[
'name' => 'headerName',
'description' => 'Some description',
],
],
];
$annot = new ApiDoc(
headers: $data['headers']
);
$array = $annot->toArray();
$this->assertArrayHasKey('headerName', $array['headers']);
$this->assertNotEmpty($array['headers']['headerName']);
$keys = array_keys($array['headers']);
$this->assertEquals($data['headers'][0]['name'], $keys[0]);
$this->assertEquals($data['headers'][0]['description'], $array['headers']['headerName']['description']);
}
public function testConstructWithOneTag(): void
{
$data = [
'tags' => 'beta',
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Single tag should be put in array');
$this->assertEquals(['beta'], $array['tags']);
}
public function testConstructWithOneTagAndColorCode(): void
{
$data = [
'tags' => [
'beta' => '#ff0000',
],
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Single tag should be put in array');
$this->assertEquals(['beta' => '#ff0000'], $array['tags']);
}
public function testConstructWithMultipleTags(): void
{
$data = [
'tags' => [
'experimental' => '#0000ff',
'beta' => '#0000ff',
],
];
$annot = new ApiDoc(
tags: $data['tags']
);
$array = $annot->toArray();
$this->assertTrue(is_array($array));
$this->assertTrue(is_array($array['tags']), 'Tags should be in array');
$this->assertEquals($data['tags'], $array['tags']);
}
public function testAlignmentOfOutputAndResponseModels(): void
{
$data = [
'output' => 'FooBar',
'responseMap' => [
400 => 'Foo\\ValidationErrorCollection',
],
];
$apiDoc = new ApiDoc(
output: $data['output'],
responseMap: $data['responseMap']
);
$map = $apiDoc->getResponseMap();
$this->assertCount(2, $map);
$this->assertArrayHasKey(200, $map);
$this->assertArrayHasKey(400, $map);
$this->assertEquals($data['output'], $map[200]);
}
public function testAlignmentOfOutputAndResponseModels2(): void
{
$data = [
'responseMap' => [
200 => 'FooBar',
400 => 'Foo\\ValidationErrorCollection',
],
];
$apiDoc = new ApiDoc(
responseMap: $data['responseMap']
);
$map = $apiDoc->getResponseMap();
$this->assertCount(2, $map);
$this->assertArrayHasKey(200, $map);
$this->assertArrayHasKey(400, $map);
$this->assertEquals($apiDoc->getOutput(), $map[200]);
}
public function testSetRoute(): void
{
$route = new Route(
'/path/{foo}',
[
'foo' => 'bar',
'nested' => [
'key1' => 'value1',
'key2' => 'value2',
],
],
[],
[],
'{foo}.awesome_host.com'
);
$apiDoc = new ApiDoc();
$apiDoc->setRoute($route);
$this->assertSame($route, $apiDoc->getRoute());
$this->assertEquals('bar.awesome_host.com', $apiDoc->getHost());
$this->assertEquals('ANY', $apiDoc->getMethod());
} }
} }

View file

@ -1,108 +0,0 @@
<?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 NelmioApiDocBundle\Tests\Command;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\ApplicationTester;
use Symfony\Component\PropertyAccess\PropertyAccess;
class DumpCommandTest extends WebTestCase
{
/**
* @dataProvider viewProvider
*
* @param string $view Command view option value
* @param array $expectedMethodsCount Expected resource methods count
* @param array $expectedMethodValues Expected resource method values
*/
public function testDumpWithViewOption($view, array $expectedMethodsCount, array $expectedMethodValues): void
{
$this->getContainer();
$application = new Application(static::$kernel);
$application->setCatchExceptions(false);
$application->setAutoExit(false);
$tester = new ApplicationTester($application);
$input = [
'command' => 'api:doc:dump',
'--view' => $view,
'--format' => 'json',
];
$tester->run($input);
$display = $tester->getDisplay();
$this->assertJson($display);
$json = json_decode($display);
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($expectedMethodsCount as $propertyPath => $expectedCount) {
$this->assertCount($expectedCount, $accessor->getValue($json, $propertyPath));
}
foreach ($expectedMethodValues as $propertyPath => $expectedValue) {
$this->assertEquals($expectedValue, $accessor->getValue($json, $propertyPath));
}
}
/**
* @return array
*/
public static function viewProvider()
{
return [
'test' => [
'test',
[
'/api/resources' => 1,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
],
],
'premium' => [
'premium',
[
'/api/resources' => 2,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
'/api/resources[1].method' => 'POST',
'/api/resources[1].uri' => '/api/resources.{_format}',
],
],
'default' => [
'default',
[
'/api/resources' => 4,
],
[
'/api/resources[0].method' => 'GET',
'/api/resources[0].uri' => '/api/resources.{_format}',
'/api/resources[1].method' => 'POST',
'/api/resources[1].uri' => '/api/resources.{_format}',
'/api/resources[2].method' => 'DELETE',
'/api/resources[2].uri' => '/api/resources/{id}.{_format}',
'/api/resources[3].method' => 'GET',
'/api/resources[3].uri' => '/api/resources/{id}.{_format}',
],
],
];
}
}

View file

@ -1,66 +0,0 @@
<?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 NelmioApiDocBundle\Tests\Controller;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
/**
* Class ApiDocControllerTest
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class ApiDocControllerTest extends WebTestCase
{
public function testSwaggerDocResourceListRoute(): void
{
$client = static::createClient();
$client->request('GET', '/api-docs');
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('text/html; charset=UTF-8', $response->headers->get('Content-type'));
}
public function dataTestApiDeclarations()
{
return [
['resources'],
['tests'],
['tests2'],
['TestResource'],
];
}
/**
* @dataProvider dataTestApiDeclarations
*/
public function testApiDeclarationRoutes($resource): void
{
$client = static::createClient();
$client->request('GET', '/api-docs/' . $resource);
$response = $client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('application/json', $response->headers->get('Content-type'));
}
public function testNonExistingApiDeclaration(): void
{
$client = static::createClient();
$client->request('GET', '/api-docs/santa');
$response = $client->getResponse();
$this->assertEquals(404, $response->getStatusCode());
}
}

View file

@ -15,14 +15,12 @@ use Nelmio\ApiDocBundle\Tests\WebTestCase;
class RequestListenerTest extends WebTestCase class RequestListenerTest extends WebTestCase
{ {
public function testDocQueryArg(): void public function testDocQueryArg()
{ {
$client = $this->createClient(); $client = $this->createClient();
$client->request('GET', '/tests?_doc=1'); $crawler = $client->request('GET', '/tests?_doc=1');
$content = $client->getResponse()->getContent(); $this->assertEquals('/tests.{_format}', trim($crawler->filter(".operation .path:contains('/tests')")->text()), 'Event listener should capture ?_doc=1 requests');
$this->assertTrue(!str_starts_with($content, '<h1>API documentation</h1>'), 'Event listener should capture ?_doc=1 requests');
$this->assertTrue(!str_starts_with($content, '/tests.{_format}'), 'Event listener should capture ?_doc=1 requests');
$client->request('GET', '/tests'); $client->request('GET', '/tests');
$this->assertEquals('tests', $client->getResponse()->getContent(), 'Event listener should let normal requests through'); $this->assertEquals('tests', $client->getResponse()->getContent(), 'Event listener should let normal requests through');

View file

@ -1,405 +0,0 @@
<?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\Tests\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class ApiDocExtractorTest extends WebTestCase
{
private static $ROUTES_QUANTITY_DEFAULT = 26; // Routes in the default view
private static $ROUTES_QUANTITY_PREMIUM = 5; // Routes in the premium view
private static $ROUTES_QUANTITY_TEST = 2; // Routes in the test view
public function testAll(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all();
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT, $data);
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache.' . ApiDoc::DEFAULT_VIEW;
$this->assertFileExists($cacheFile);
$this->assertStringEqualsFile($cacheFile, serialize($data));
foreach ($data as $key => $d) {
$this->assertTrue(is_array($d));
$this->assertArrayHasKey('annotation', $d);
$this->assertArrayHasKey('resource', $d);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $d['annotation']);
$this->assertInstanceOf('Symfony\Component\Routing\Route', $d['annotation']->getRoute());
$this->assertNotNull($d['resource']);
}
}
public function testRouteVersionChecking(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->allForVersion('1.5');
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT, $data);
$data = $extractor->allForVersion('1.4');
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_DEFAULT - 1, $data);
}
public function testGet(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'test_route_1');
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$this->assertTrue($annotation->isResource());
$this->assertEquals('index action', $annotation->getDescription());
$array = $annotation->toArray();
$this->assertTrue(is_array($array['filters']));
$this->assertNull($annotation->getInput());
$annotation2 = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'test_service_route_1');
$annotation2->getRoute()
->setDefault('_controller', $annotation->getRoute()->getDefault('_controller'))
->compile() // compile as we changed a default value
;
$this->assertEquals($annotation, $annotation2);
}
public function testGetWithBadController(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Undefined\Controller::indexAction', 'test_route_1');
$this->assertNull($data);
$data = $extractor->get('undefined_service:index', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithBadRoute(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'invalid_route');
$this->assertNull($data);
$data = $extractor->get('nelmio.test.controller:indexAction', 'invalid_route');
$this->assertNull($data);
}
public function testGetWithInvalidPath(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController', 'test_route_1');
$this->assertNull($data);
$data = $extractor->get('nelmio.test.controller', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithMethodWithoutApiDocAnnotation(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::anotherAction', 'test_route_3');
$this->assertNull($data);
$data = $extractor->get('nelmio.test.controller:anotherAction', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithDocComment(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::myCommentedAction', 'test_route_5');
$this->assertNotNull($annotation);
$this->assertEquals(
'This method is useful to test if the getDocComment works.',
$annotation->getDescription()
);
$data = $annotation->toArray();
$this->assertEquals(
4,
count($data['requirements'])
);
$this->assertEquals(
'The param type',
$data['requirements']['paramType']['description']
);
$this->assertEquals(
'The param id',
$data['requirements']['param']['description']
);
}
public function testGetWithDeprecated(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::DeprecatedAction', 'test_route_14');
$this->assertNotNull($annotation);
$this->assertTrue(
$annotation->getDeprecated()
);
}
public function testOutputWithSelectedParsers(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zReturnSelectedParsersOutputAction', 'test_route_19');
$this->assertNotNull($annotation);
$output = $annotation->getOutput();
$parsers = $output['parsers'];
$this->assertEquals(
'Nelmio\\ApiDocBundle\\Parser\\JmsMetadataParser',
$parsers[0]
);
$this->assertEquals(
'Nelmio\\ApiDocBundle\\Parser\\ValidationParser',
$parsers[1]
);
$this->assertCount(2, $parsers);
}
public function testInputWithSelectedParsers(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::zReturnSelectedParsersInputAction', 'test_route_20');
$this->assertNotNull($annotation);
$input = $annotation->getInput();
$parsers = $input['parsers'];
$this->assertEquals(
'Nelmio\\ApiDocBundle\\Parser\\FormTypeParser',
$parsers[0]
);
$this->assertCount(1, $parsers);
}
public function testPostRequestDoesRequireParametersWhenMarkedAsSuch(): void
{
$container = $this->getContainer();
/** @var ApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
/** @var ApiDoc $annotation */
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::requiredParametersAction', 'test_required_parameters');
$parameters = $annotation->getParameters();
$this->assertTrue($parameters['required_field']['required']);
}
public function testPatchRequestDoesNeverRequireParameters(): void
{
$container = $this->getContainer();
/** @var ApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
/** @var ApiDoc $annotation */
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::requiredParametersAction', 'test_patch_disables_required_parameters');
$parameters = $annotation->getParameters();
$this->assertFalse($parameters['required_field']['required']);
}
public static function dataProviderForViews(): array
{
$offset = 0;
return [
['default', self::$ROUTES_QUANTITY_DEFAULT + $offset],
['premium', self::$ROUTES_QUANTITY_PREMIUM + $offset],
['test', self::$ROUTES_QUANTITY_TEST + $offset],
['foobar', $offset],
['', $offset],
[null, $offset],
];
}
public function testViewNamedTest(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all('test');
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_TEST, $data);
$a1 = $data[0]['annotation'];
$this->assertCount(3, $a1->getViews());
$this->assertEquals('List resources.', $a1->getDescription());
$a2 = $data[1]['annotation'];
$this->assertCount(2, $a2->getViews());
$this->assertEquals('create another test', $a2->getDescription());
}
public function testViewNamedPremium(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all('premium');
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(self::$ROUTES_QUANTITY_PREMIUM, $data);
$a1 = $data[0]['annotation'];
$this->assertCount(2, $a1->getViews());
$this->assertEquals('List another resource.', $a1->getDescription());
$a2 = $data[1]['annotation'];
$this->assertCount(3, $a2->getViews());
$this->assertEquals('List resources.', $a2->getDescription());
}
/**
* @dataProvider dataProviderForViews
*/
public function testForViews($view, $count): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all($view);
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount($count, $data);
}
public function testOverrideJmsAnnotationWithApiDocParameters(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get(
'Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::overrideJmsAnnotationWithApiDocParametersAction',
'test_route_27'
);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$array = $annotation->toArray();
$this->assertTrue(is_array($array['parameters']));
$this->assertEquals('string', $array['parameters']['foo']['dataType']);
$this->assertEquals('DateTime', $array['parameters']['bar']['dataType']);
$this->assertEquals('integer', $array['parameters']['number']['dataType']);
$this->assertEquals('string', $array['parameters']['number']['actualType']);
$this->assertNull($array['parameters']['number']['subType']);
$this->assertTrue($array['parameters']['number']['required']);
$this->assertEquals('This is the new description', $array['parameters']['number']['description']);
$this->assertFalse($array['parameters']['number']['readonly']);
$this->assertEquals('v3.0', $array['parameters']['number']['sinceVersion']);
$this->assertEquals('v4.0', $array['parameters']['number']['untilVersion']);
$this->assertEquals('object (ArrayCollection)', $array['parameters']['arr']['dataType']);
$this->assertEquals('object (JmsNested)', $array['parameters']['nested']['dataType']);
$this->assertEquals('integer', $array['parameters']['nested']['children']['bar']['dataType']);
$this->assertEquals('d+', $array['parameters']['nested']['children']['bar']['format']);
}
public function testJmsAnnotation(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get(
'Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::defaultJmsAnnotations',
'test_route_27'
);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Attribute\ApiDoc', $annotation);
$array = $annotation->toArray();
$this->assertTrue(is_array($array['parameters']));
$this->assertEquals('string', $array['parameters']['foo']['dataType']);
$this->assertEquals('DateTime', $array['parameters']['bar']['dataType']);
$this->assertEquals('double', $array['parameters']['number']['dataType']);
$this->assertEquals('float', $array['parameters']['number']['actualType']);
$this->assertNull($array['parameters']['number']['subType']);
$this->assertFalse($array['parameters']['number']['required']);
$this->assertEquals('', $array['parameters']['number']['description']);
$this->assertFalse($array['parameters']['number']['readonly']);
$this->assertNull($array['parameters']['number']['sinceVersion']);
$this->assertNull($array['parameters']['number']['untilVersion']);
$this->assertEquals('array', $array['parameters']['arr']['dataType']);
$this->assertEquals('object (JmsNested)', $array['parameters']['nested']['dataType']);
$this->assertEquals('string', $array['parameters']['nested']['children']['bar']['dataType']);
}
public function testMergeParametersDefaultKeyNotExistingInFirstArray(): void
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$mergeMethod = new \ReflectionMethod('Nelmio\ApiDocBundle\Extractor\ApiDocExtractor', 'mergeParameters');
$mergeMethod->setAccessible(true);
$p1 = [
'myPropName' => [
'dataType' => 'string',
'actualType' => 'string',
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
],
];
$p2 = [
'myPropName' => [
'dataType' => 'string',
'actualType' => 'string',
'subType' => null,
'required' => null,
'description' => null,
'readonly' => null,
'default' => '',
],
];
$mergedResult = $mergeMethod->invokeArgs($extractor, [$p1, $p2]);
$this->assertEquals($p2, $mergedResult);
}
}

View file

@ -0,0 +1,208 @@
<?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\Tests\Extractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class ApiDocExtractorTest extends WebTestCase
{
public function testAll()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
set_error_handler(array($this, 'handleDeprecation'));
$data = $extractor->all();
restore_error_handler();
$this->assertTrue(is_array($data));
$this->assertCount(16, $data);
foreach ($data as $d) {
$this->assertTrue(is_array($d));
$this->assertArrayHasKey('annotation', $d);
$this->assertArrayHasKey('resource', $d);
$this->assertInstanceOf('Nelmio\ApiDocBundle\Annotation\ApiDoc', $d['annotation']);
$this->assertInstanceOf('Symfony\Component\Routing\Route', $d['annotation']->getRoute());
$this->assertNotNull($d['resource']);
}
$a1 = $data[0]['annotation'];
$array1 = $a1->toArray();
$this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput());
$a1 = $data[1]['annotation'];
$array1 = $a1->toArray();
$this->assertTrue($a1->isResource());
$this->assertEquals('index action', $a1->getDescription());
$this->assertTrue(is_array($array1['filters']));
$this->assertNull($a1->getInput());
$a2 = $data[2]['annotation'];
$array2 = $a2->toArray();
$this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a2 = $data[3]['annotation'];
$array2 = $a2->toArray();
$this->assertFalse($a2->isResource());
$this->assertEquals('create test', $a2->getDescription());
$this->assertFalse(isset($array2['filters']));
$this->assertEquals('Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType', $a2->getInput());
$a3 = $data['12']['annotation'];
$this->assertTrue($a3->getHttps());
}
public function testGet()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'test_route_1');
$this->assertInstanceOf('Nelmio\ApiDocBundle\Annotation\ApiDoc', $annotation);
$this->assertTrue($annotation->isResource());
$this->assertEquals('index action', $annotation->getDescription());
$array = $annotation->toArray();
$this->assertTrue(is_array($array['filters']));
$this->assertNull($annotation->getInput());
$annotation2 = $extractor->get('nemlio.test.controller:indexAction', 'test_service_route_1');
$annotation2->getRoute()
->setDefault('_controller', $annotation->getRoute()->getDefault('_controller'))
->compile(); // compile as we changed a default value
$this->assertEquals($annotation, $annotation2);
}
public function testGetWithBadController()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Undefined\Controller::indexAction', 'test_route_1');
$this->assertNull($data);
$data = $extractor->get('undefined_service:index', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithBadRoute()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::indexAction', 'invalid_route');
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller:indexAction', 'invalid_route');
$this->assertNull($data);
}
public function testGetWithInvalidPattern()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController', 'test_route_1');
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithMethodWithoutApiDocAnnotation()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$data = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::anotherAction', 'test_route_3');
$this->assertNull($data);
$data = $extractor->get('nemlio.test.controller:anotherAction', 'test_service_route_1');
$this->assertNull($data);
}
public function testGetWithDocComment()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::myCommentedAction', 'test_route_5');
$this->assertNotNull($annotation);
$this->assertEquals(
"This method is useful to test if the getDocComment works.",
$annotation->getDescription()
);
$data = $annotation->toArray();
$this->assertEquals(
4,
count($data['requirements'])
);
$this->assertEquals(
'The param type',
$data['requirements']['paramType']['description']
);
$this->assertEquals(
'The param id',
$data['requirements']['param']['description']
);
}
public function testGetWithAuthentication()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::AuthenticatedAction', 'test_route_13');
$this->assertNotNull($annotation);
$this->assertTrue(
$annotation->getAuthentication()
);
}
public function testGetWithCache()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::CachedAction', 'test_route_14');
$this->assertNotNull($annotation);
$this->assertEquals(
60,
$annotation->getCache()
);
}
public function testGetWithDeprecated()
{
$container = $this->getContainer();
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$annotation = $extractor->get('Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController::DeprecatedAction', 'test_route_14');
$this->assertNotNull($annotation);
$this->assertTrue(
$annotation->getDeprecated()
);
}
}

View file

@ -1,94 +0,0 @@
<?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\Tests\Extractor;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor;
use Nelmio\ApiDocBundle\Tests\WebTestCase;
class CachingApiDocExtractorTest extends WebTestCase
{
/**
* @return array
*/
public static function viewsWithoutDefaultProvider()
{
$data = ApiDocExtractorTest::dataProviderForViews();
// remove default view data from provider
array_shift($data);
return $data;
}
/**
* Test that every view cache is saved in its own cache file
*
* @dataProvider viewsWithoutDefaultProvider
*
* @param string $view View name
*/
public function testDifferentCacheFilesAreCreatedForDifferentViews($view): void
{
$container = $this->getContainer();
/* @var CachingApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$this->assertInstanceOf('\Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor', $extractor);
set_error_handler([$this, 'handleDeprecation']);
$defaultData = $extractor->all(ApiDoc::DEFAULT_VIEW);
$data = $extractor->all($view);
restore_error_handler();
$this->assertIsArray($data);
$this->assertNotSameSize($defaultData, $data);
$this->assertNotEquals($defaultData, $data);
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache';
$expectedDefaultViewCacheFile = $cacheFile . '.' . ApiDoc::DEFAULT_VIEW;
$expectedViewCacheFile = $cacheFile . '.' . $view;
$this->assertFileExists($expectedDefaultViewCacheFile);
$this->assertFileExists($expectedViewCacheFile);
$this->assertFileNotEquals($expectedDefaultViewCacheFile, $expectedViewCacheFile);
}
/**
* @dataProvider \Nelmio\ApiDocBundle\Tests\Extractor\ApiDocExtractorTest::dataProviderForViews
*
* @param string $view View name to test
*/
public function testCachedResultSameAsGenerated($view): void
{
$container = $this->getContainer();
/* @var CachingApiDocExtractor $extractor */
$extractor = $container->get('nelmio_api_doc.extractor.api_doc_extractor');
$this->assertInstanceOf('\Nelmio\ApiDocBundle\Extractor\CachingApiDocExtractor', $extractor);
$cacheFile = $container->getParameter('kernel.cache_dir') . '/api-doc.cache';
$expectedViewCacheFile = $cacheFile . '.' . $view;
set_error_handler([$this, 'handleDeprecation']);
$data = $extractor->all($view);
$this->assertFileExists($expectedViewCacheFile);
$cachedData = $extractor->all($view);
restore_error_handler();
$this->assertIsArray($data);
$this->assertIsArray($cachedData);
$this->assertSameSize($data, $cachedData);
$this->assertEquals($data, $cachedData);
}
}

View file

@ -1,116 +0,0 @@
<?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\Tests\Extractor;
use PHPUnit\Framework\TestCase;
class CollectionDirectiveTest extends TestCase
{
/**
* @var TestExtractor
*/
private $testExtractor;
protected function setUp(): void
{
$this->testExtractor = new TestExtractor();
}
private function normalize($input)
{
return $this->testExtractor->getNormalization($input);
}
/**
* @dataProvider dataNormalizationTests
*/
public function testNormalizations($input, callable $callable): void
{
call_user_func($callable, $this->normalize($input), $this);
}
public function dataNormalizationTests()
{
return [
'test_simple_notation' => [
'array<User>',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('', $actual['collectionName']);
$case->assertEquals('User', $actual['class']);
},
],
'test_simple_notation_with_namespaces' => [
'array<Vendor0_2\\_Namespace1\\Namespace_2\\User>',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('', $actual['collectionName']);
$case->assertEquals('Vendor0_2\\_Namespace1\\Namespace_2\\User', $actual['class']);
},
],
'test_simple_named_collections' => [
'array<Group> as groups',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('groups', $actual['collectionName']);
$case->assertEquals('Group', $actual['class']);
},
],
'test_namespaced_named_collections' => [
'array<_Vendor\\Namespace0\\Namespace_2F3\\Group> as groups',
function ($actual, TestCase $case): void {
$case->assertArrayHasKey('collection', $actual);
$case->assertArrayHasKey('collectionName', $actual);
$case->assertArrayHasKey('class', $actual);
$case->assertTrue($actual['collection']);
$case->assertEquals('groups', $actual['collectionName']);
$case->assertEquals('_Vendor\\Namespace0\\Namespace_2F3\\Group', $actual['class']);
},
],
];
}
/**
* @dataProvider dataInvalidDirectives
*/
public function testInvalidDirectives($input): void
{
$this->expectException(\InvalidArgumentException::class);
$this->normalize($input);
}
public function dataInvalidDirectives()
{
return [
['array<>'],
['array<Vendor\\>'],
['array<2Vendor\\>'],
['array<Vendor\\2Class>'],
['array<User> as'],
['array<User> as '],
];
}
}

View file

@ -1,26 +0,0 @@
<?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\Tests\Extractor;
use Nelmio\ApiDocBundle\Extractor\ApiDocExtractor;
class TestExtractor extends ApiDocExtractor
{
public function __construct()
{
}
public function getNormalization($input)
{
return $this->normalizeClassParameter($input);
}
}

View file

@ -1,73 +0,0 @@
<?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\Tests\Fixtures\Controller;
use Nelmio\ApiDocBundle\Attribute\ApiDoc;
class ResourceController
{
#[ApiDoc(
resource: true,
views: ['test', 'premium', 'default'],
resourceDescription: 'Operations on resource.',
description: 'List resources.',
output: "array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test> as tests",
statusCodes: [200 => 'Returned on success.', 404 => 'Returned if resource cannot be found.']
)]
public function listResourcesAction(): void
{
}
#[ApiDoc(description: 'Retrieve a resource by ID.')]
public function getResourceAction(): void
{
}
#[ApiDoc(description: 'Delete a resource by ID.')]
public function deleteResourceAction(): void
{
}
#[ApiDoc(
description: 'Create a new resource.',
views: ['default', 'premium'],
input: ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", 'name' => ''],
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsNested",
responseMap: [
400 => ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Form\SimpleType", 'form_errors' => true],
]
)]
public function createResourceAction(): void
{
}
#[ApiDoc(
resource: true,
views: ['default', 'premium'],
description: 'List another resource.',
resourceDescription: 'Operations on another resource.',
output: "array<Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest>"
)]
public function listAnotherResourcesAction(): void
{
}
#[ApiDoc(description: 'Retrieve another resource by ID.')]
public function getAnotherResourceAction(): void
{
}
#[ApiDoc(description: 'Update a resource bu ID.')]
public function updateAnotherResourceAction(): void
{
}
}

View file

@ -11,72 +11,57 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller; namespace Nelmio\ApiDocBundle\Tests\Fixtures\Controller;
use Nelmio\ApiDocBundle\Attribute\ApiDoc; use FOS\RestBundle\Controller\Annotations\QueryParam;
use Nelmio\ApiDocBundle\Tests\Fixtures\DependencyTypePath; use FOS\RestBundle\Controller\Annotations\RequestParam;
use Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
class TestController class TestController
{ {
#[ApiDoc( /**
resource: 'TestResource', * @ApiDoc(
views: 'default' * resource=true,
)] * description="index action",
public function namedResourceAction(): void * filters={
{ * {"name"="a", "dataType"="integer"},
} * {"name"="b", "dataType"="string", "arbitrary"={"arg1", "arg2"}}
* }
#[ApiDoc( * )
resource: true, */
description: 'index action',
filters: [
['name' => 'a', 'dataType' => 'integer'],
['name' => 'b', 'dataType' => 'string', 'arbitrary' => ['arg1', 'arg2']],
],
)]
public function indexAction() public function indexAction()
{ {
return new Response('tests'); return new Response('tests');
} }
#[ApiDoc( /**
resource: true, * @ApiDoc(
description: 'create test', * description="create test",
views: ['default', 'premium'], * input="Nelmio\ApiDocBundle\Tests\Fixtures\Form\TestType"
input: TestType::class * )
)] */
public function postTestAction(): void public function postTestAction()
{ {
} }
#[ApiDoc( /**
description: 'post test 2', * @ApiDoc(
views: ['default', 'premium'], * description="post test 2",
resource: true * resource=true
)] * )
public function postTest2Action(): void */
public function postTest2Action()
{ {
} }
#[ApiDoc( public function anotherAction()
description: 'Action with required parameters',
input: "Nelmio\ApiDocBundle\Tests\Fixtures\Form\RequiredType"
)]
public function requiredParametersAction(): void
{ {
} }
public function anotherAction(): void /**
{ * @ApiDoc(description="Action without HTTP verb")
} */
public function anyAction()
#[ApiDoc]
public function routeVersionAction(): void
{
}
#[ApiDoc(description: 'Action without HTTP verb')]
public function anyAction(): void
{ {
} }
@ -84,193 +69,99 @@ class TestController
* This method is useful to test if the getDocComment works. * This method is useful to test if the getDocComment works.
* And, it supports multilines until the first '@' char. * And, it supports multilines until the first '@' char.
* *
* @param int $id A nice comment * @ApiDoc()
*
* @param int $id A nice comment
* @param int $page * @param int $page
* @param int $paramType The param type * @param int $paramType The param type
* @param int $param The param id * @param int $param The param id
*/ */
#[ApiDoc] public function myCommentedAction()
public function myCommentedAction($id, $page, int $paramType, int $param): void
{
}
#[ApiDoc]
public function yetAnotherAction(): void
{
}
#[ApiDoc(
views: ['default', 'test'],
description: 'create another test',
input: DependencyTypePath::TYPE
)]
public function anotherPostAction(): void
{
}
#[ApiDoc(
description: 'Testing JMS',
input: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
)]
public function jmsInputTestAction(): void
{
}
#[ApiDoc(
description: 'Testing return',
output: DependencyTypePath::TYPE
)]
public function jmsReturnTestAction(): void
{
}
#[ApiDoc]
public function zCachedAction(): void
{
}
#[ApiDoc]
public function zSecuredAction(): void
{ {
} }
/** /**
* @ApiDoc()
*/
public function yetAnotherAction()
{
}
/**
* @ApiDoc(
* description="create another test",
* input="dependency_type"
* )
*/
public function anotherPostAction()
{
}
/**
* @ApiDoc()
* @QueryParam(name="page", requirements="\d+", default="1", description="Page of the overview.")
*/
public function zActionWithQueryParamAction()
{
}
/**
* @ApiDoc(
* description="Testing JMS",
* input="Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
* )
*/
public function jmsInputTestAction()
{
}
/**
* @ApiDoc(
* description="Testing return",
* output="dependency_type"
* )
*/
public function jmsReturnTestAction()
{
}
/**
* @ApiDoc()
* @RequestParam(name="param1", requirements="string", description="Param1 description.")
*/
public function zActionWithRequestParamAction()
{
}
/**
* @ApiDoc()
*/
public function secureRouteAction()
{
}
/**
* @ApiDoc(
* authentication=true
* )
*/
public function authenticatedAction()
{
}
/**
* @ApiDoc()
* @Cache(maxage=60, public=1)
*/
public function cachedAction()
{
}
/**
* @ApiDoc()
* @deprecated * @deprecated
*/ */
#[ApiDoc] public function deprecatedAction()
public function deprecatedAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"
)]
public function jmsReturnNestedOutputAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsChild"
)]
public function jmsReturnNestedExtendedOutputAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest"
)]
public function zReturnJmsAndValidationOutputAction(): void
{
}
#[ApiDoc(
description: 'Returns a collection of Object',
requirements: [
['name' => 'limit', 'dataType' => 'integer', 'requirement' => "\d+", 'description' => 'how many objects to return'],
],
parameters: [
['name' => 'categoryId', 'dataType' => 'integer', 'required' => true, 'description' => 'category id'],
]
)]
public function cgetAction($id): void
{
}
#[ApiDoc(
input: [
'class' => TestType::class,
'parsers' => ["Nelmio\ApiDocBundle\Parser\FormTypeParser"],
]
)]
public function zReturnSelectedParsersInputAction(): void
{
}
#[ApiDoc(
output: [
'class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\MultipleTest",
'parsers' => [
"Nelmio\ApiDocBundle\Parser\JmsMetadataParser",
"Nelmio\ApiDocBundle\Parser\ValidationParser",
],
]
)]
public function zReturnSelectedParsersOutputAction(): void
{
}
#[ApiDoc(
section: 'private'
)]
public function privateAction(): void
{
}
#[ApiDoc(
section: 'exclusive'
)]
public function exclusiveAction(): void
{
}
/**
* @see http://symfony.com
*/
#[ApiDoc]
public function withLinkAction(): void
{
}
#[ApiDoc(
input: ['class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest"],
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
parameters: [
[
'name' => 'number',
'dataType' => 'integer',
'actualType' => 'string',
'subType' => null,
'required' => true,
'description' => 'This is the new description',
'readonly' => false,
'sinceVersion' => 'v3.0',
'untilVersion' => 'v4.0',
],
[
'name' => 'arr',
'dataType' => 'object (ArrayCollection)',
],
[
'name' => 'nested',
'dataType' => 'object (JmsNested)',
'children' => [
'bar' => [
'dataType' => 'integer',
'format' => 'd+',
],
],
],
]
)]
public function overrideJmsAnnotationWithApiDocParametersAction(): void
{
}
#[ApiDoc(
output: "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
input: [
'class' => "Nelmio\ApiDocBundle\Tests\Fixtures\Model\JmsTest",
],
)]
public function defaultJmsAnnotations(): void
{
}
#[ApiDoc(
description: 'Route with host placeholder',
views: ['default']
)]
public function routeWithHostAction(): void
{ {
} }
} }

View file

@ -1,32 +0,0 @@
<?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\Tests\Fixtures;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
/*
* This class is used to have dynamic annotations for BC.
* {@see Nelmio\ApiDocBundle\Tests\Fixtures\Controller\TestController}
*
* @author Ener-Getick <egetick@gmail.com>
*/
if (LegacyFormHelper::isLegacy()) {
class DependencyTypePath
{
public const TYPE = 'dependency_type';
}
} else {
class DependencyTypePath
{
public const TYPE = 'Nelmio\ApiDocBundle\Tests\Fixtures\Form\DependencyType';
}
}

View file

@ -1,46 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class CollectionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$collectionType = 'Symfony\Component\Form\Extension\Core\Type\CollectionType';
$builder
->add('a', LegacyFormHelper::getType($collectionType), [
LegacyFormHelper::hasBCBreaks() ? 'entry_type' : 'type' => LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'),
])
->add('b', LegacyFormHelper::getType($collectionType), [
LegacyFormHelper::hasBCBreaks() ? 'entry_type' : 'type' => LegacyFormHelper::isLegacy() ? new TestType() : __NAMESPACE__ . '\TestType',
])
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'collection_type';
}
}

View file

@ -1,46 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class CompoundType
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class CompoundType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('sub_form', LegacyFormHelper::isLegacy() ? new SimpleType() : __NAMESPACE__ . '\SimpleType')
->add('a', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\NumberType'))
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}
}

View file

@ -13,7 +13,6 @@ namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DependencyType extends AbstractType class DependencyType extends AbstractType
@ -22,40 +21,29 @@ class DependencyType extends AbstractType
{ {
} }
public function buildForm(FormBuilderInterface $builder, array $options): void /**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
->add('a', null, ['description' => 'A nice description']) ->add('a', null, array('description' => 'A nice description'))
; ;
} }
/** /**
* @deprecated Remove it when bumping requirements to Symfony 2.7+ * {@inheritdoc}
*/ */
public function setDefaultOptions(OptionsResolverInterface $resolver): void public function setDefaultOptions(OptionsResolverInterface $resolver)
{ {
$this->configureOptions($resolver); $resolver->setDefaults(array(
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', 'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
]); ));
return; return;
} }
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName() public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{ {
return 'dependency_type'; return 'dependency_type';
} }

View file

@ -1,56 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EntityType extends AbstractType
{
/**
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\EntityTest',
]);
return;
}
public function getParent()
{
return LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType');
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'entity';
}
}

View file

@ -1,79 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ImprovedTestType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choiceType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType');
$datetimeType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateTimeType');
$dateType = LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateType');
$builder
->add('dt1', $datetimeType, ['widget' => 'single_text', 'description' => 'A nice description'])
->add('dt2', $datetimeType, ['date_format' => 'M/d/y', 'html5' => false])
->add('dt3', $datetimeType, ['widget' => 'single_text', 'format' => 'M/d/y H:i:s', 'html5' => false])
->add('dt4', $datetimeType, ['date_format' => \IntlDateFormatter::MEDIUM])
->add('dt5', $datetimeType, ['format' => 'M/d/y H:i:s', 'html5' => false])
->add('d1', $dateType, ['format' => \IntlDateFormatter::MEDIUM])
->add('d2', $dateType, ['format' => 'd-M-y'])
->add('c1', $choiceType, ['choices' => ['Male' => 'm', 'Female' => 'f']])
->add('c2', $choiceType, ['choices' => ['Male' => 'm', 'Female' => 'f'], 'multiple' => true])
->add('c3', $choiceType, ['choices' => []])
->add('c4', $choiceType, ['choices' => ['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']]])
->add('e1', LegacyFormHelper::isLegacy() ? new EntityType() : __NAMESPACE__ . '\EntityType',
LegacyFormHelper::isLegacy()
? ['choice_list' => new SimpleChoiceList(['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']])]
: ['choices' => ['bar' => 'foo', 'bazgroup' => ['Buzz' => 'baz']]]
)
;
}
/**
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\ImprovedTest',
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}
}

View file

@ -1,69 +0,0 @@
<?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\Tests\Fixtures\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RequireConstructionType extends AbstractType
{
private $noThrow;
public function __construct($optionalArgs = null)
{
$this->noThrow = true;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if (true !== $this->noThrow) {
throw new \RuntimeException(__CLASS__ . ' require contruction');
}
$builder
->add('a', null, ['description' => 'A nice description'])
;
}
/**
* @deprecated Remove it when bumping requirements to Symfony 2.7+
*/
public function setDefaultOptions(OptionsResolverInterface $resolver): void
{
$this->configureOptions($resolver);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
]);
return;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'require_construction_type';
}
}

View file

@ -1,43 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class SimpleType
*
* @author Lucas van Lierop <lucas@vanlierop.org>
*/
class RequiredType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('required_field', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), ['required' => true]);
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return '';
}
}

View file

@ -1,53 +0,0 @@
<?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\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Class SimpleType
*
* @author Bez Hermoso <bez@activelamp.com>
*/
class SimpleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('a', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), [
'description' => 'Something that describes A.',
])
->add('b', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\NumberType'))
->add('c', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\ChoiceType'),
['choices' => ['X' => 'x', 'Y' => 'y', 'Z' => 'z']]
)
->add('d', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateTimeType'))
->add('e', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\DateType'))
->add('g', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextareaType'))
;
}
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
public function getBlockPrefix()
{
return 'simple';
}
}

View file

@ -11,52 +11,38 @@
namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form; namespace Nelmio\ApiDocBundle\Tests\Fixtures\Form;
use Nelmio\ApiDocBundle\Util\LegacyFormHelper;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TestType extends AbstractType class TestType extends AbstractType
{ {
public function buildForm(FormBuilderInterface $builder, array $options): void /**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{ {
$builder $builder
->add('a', null, ['description' => 'A nice description']) ->add('a', null, array('description' => 'A nice description'))
->add('b') ->add('b')
->add($builder->create('c', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\CheckboxType'))) ->add($builder->create('c', 'checkbox'))
->add('d', LegacyFormHelper::getType('Symfony\Component\Form\Extension\Core\Type\TextType'), ['data' => 'DefaultTest'])
; ;
} }
/** /**
* @deprecated Remove it when bumping requirements to Symfony 2.7+ * {@inheritdoc}
*/ */
public function setDefaultOptions(OptionsResolverInterface $resolver): void public function setDefaultOptions(OptionsResolverInterface $resolver)
{ {
$this->configureOptions($resolver); $resolver->setDefaults(array(
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test', 'data_class' => 'Nelmio\ApiDocBundle\Tests\Fixtures\Model\Test',
]); ));
return; return;
} }
/**
* BC SF < 2.8
* {@inheritdoc}
*/
public function getName() public function getName()
{ {
return $this->getBlockPrefix(); return 'test_type';
}
public function getBlockPrefix()
{
return '';
} }
} }

View file

@ -1,16 +0,0 @@
<?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\Tests\Fixtures\Model;
class EntityTest
{
}

View file

@ -1,28 +0,0 @@
<?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\Tests\Fixtures\Model;
class ImprovedTest
{
public $dt1;
public $dt2;
public $dt3;
public $dt4;
public $dt5;
public $d1;
public $d2;
public $c1;
public $c2;
public $c3;
public $c4;
public $e1;
}

Some files were not shown because too many files have changed in this diff Show more